Правильная реализация распределенных блокировок Redis (Java Edition)

Java Redis задняя часть Lua

Этот блог использует сторонний компонент с открытым исходным кодом Jedis для реализации клиента Redis и рассматривает только сценарий развертывания сервера Redis на одном компьютере.

предисловие

Обычно существует три способа реализации распределенных блокировок: 1. Оптимистичные блокировки базы данных 2. Распределенные блокировки на основе Redis 3. Распределенные блокировки на основе ZooKeeper. В этом блоге будет представлен второй способ реализации распределенных блокировок на основе Redis. Хотя в Интернете есть различные блоги, в которых рассказывается о реализации распределенных блокировок Redis, их реализации имеют различные проблемы.Во избежание недоразумений в этом блоге подробно рассказывается, как правильно реализовать распределенные блокировки Redis.


надежность

Во-первых, чтобы обеспечить доступность распределенных блокировок, мы должны как минимум убедиться, что реализация блокировки соответствует следующим четырем условиям:

  1. взаимная исключительность.В любой момент только один клиент может удерживать блокировку.
  2. Взаимной блокировки не произойдет.Даже если клиент выйдет из строя, удерживая блокировку, и не разблокирует ее активно, другие клиенты могут быть гарантированно заблокированы в будущем.
  3. Отказоустойчивой.Клиенты могут блокировать и разблокировать, пока большинство узлов Redis запущены и работают.
  4. Беда должна положить этому конец.Блокировку и разблокировку должен выполнять один и тот же клиент, и клиент не может разблокировать блокировки, добавленные другими.

Код

зависимости компонентов

Сначала нам нужно импортировать через MavenJedisкомпоненты с открытым исходным кодом, вpom.xmlДобавьте в файл следующий код:

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.9.0</version>
</dependency>

код блокировки

правильная осанка

Говорить дешево, покажи мне код. Сначала покажите код, а затем медленно объясните, почему он реализован так:

public class RedisTool {

    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";

    /**
     * 尝试获取分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @param expireTime 超期时间
     * @return 是否获取成功
     */
    public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {

        String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);

        if (LOCK_SUCCESS.equals(result)) {
            return true;
        }
        return false;

    }

}

Как видите, мы блокируем только одну строку кода:jedis.set(String key, String value, String nxxx, String expx, int time), метод set() имеет всего пять параметров:

  • Первый — это ключ, мы используем ключ в качестве замка, потому что ключ уникален.

  • Второй это значение, мы передаем requestId, многие детские ботиночки могут не понять, разве недостаточно иметь ключ в качестве замка, зачем использовать значение? Причина в том, что когда мы говорили о надежности выше, распределенные блокировки должны удовлетворять четвертому условию.Проблема должна закончиться, присваивая значение requestId, мы знаем, к какому запросу добавляется блокировка, и можем иметь основу для его разблокировки. можно использовать requestIdUUID.randomUUID().toString()метод генерации.

  • Третий - nxxx, для этого параметра заполняем NX, что означает SET IF NOT EXIST, то есть когда ключ не существует, выполняем операцию set, если ключ уже есть, ничего не делаем;

  • Четвертый — expx, и этот параметр, который мы передаем, — PX, а это значит, что нам нужно добавить в этот ключ настройку с истекшим сроком действия, а конкретное время определяется пятым параметром.

  • Пятый — это время, которое соответствует четвертому параметру и представляет собой время истечения срока действия ключа.

В общем, выполнение вышеприведенного метода set() приведет только к двум результатам: 1. В настоящее время блокировки нет (ключ не существует), затем выполняется операция блокировки, и для блокировки устанавливается срок действия, и значение указывает на клиент блокировки. 2. Есть блокировка, ничего не делать.

Осторожная детская обувь найдет его, наш код блокировки нас удовлетворяетнадежностьТри состояния, описанные в Прежде всего, set() добавляет параметр NX, который может гарантировать, что при наличии существующего ключа функция не будет вызвана успешно, то есть только один клиент может удерживать блокировку, что удовлетворяет взаимному исключению. Во-вторых, поскольку мы устанавливаем время истечения срока действия замка, даже если держатель замка выйдет из строя и не сможет его разблокировать, замок будет автоматически разблокирован (то есть ключ будет удален) по истечении срока действия, и никакая взаимоблокировка не будет происходить. Наконец, поскольку мы присваиваем значение requestId, которое представляет идентификатор запроса заблокированного клиента, мы можем проверить, является ли это тем же самым клиентом, когда клиент разблокирован. Поскольку мы рассматриваем только сценарий развертывания Redis на одной машине, мы пока не рассматриваем отказоустойчивость.

Пример ошибки 1

Более распространенным примером ошибки является использованиеjedis.setnx()иjedis.expire()Комбинация для достижения блокировки, код выглядит следующим образом:

public static void wrongGetLock1(Jedis jedis, String lockKey, String requestId, int expireTime) {

    Long result = jedis.setnx(lockKey, requestId);
    if (result == 1) {
        // 若在这里程序突然崩溃,则无法设置过期时间,将发生死锁
        jedis.expire(lockKey, expireTime);
    }

}

Функция метода setnx() — SET IF NOT EXIST, а метод expire() — добавление срока действия блокировки. На первый взгляд кажется, что это то же самое, что и результат предыдущего метода set(). Однако, поскольку это две команды Redis, они не являются атомарными. Если программа внезапно падает после выполнения setnx(), блокировка не иметь установленный срок годности. Тогда произойдет тупик. Причина, по которой кто-то реализует это в Интернете, заключается в том, что более ранняя версия jedis не поддерживает метод set() с несколькими параметрами.

Пример ошибки 2

Такой пример ошибки найти сложнее, а реализация сложнее. Идея реализации: использоватьjedis.setnx()Команда реализует блокировку, где ключ — это блокировка, а значение — время истечения срока действия блокировки. Процесс выполнения: 1. Попробуйте заблокировать через метод setnx().Если текущая блокировка не существует, он успешно вернет блокировку. 2. Если блокировка уже существует, получить время истечения блокировки и сравнить его с текущим временем.Если срок действия блокировки истек, установить новое время истечения срока действия и вернуть успех блокировки. код показывает, как показано ниже:

public static boolean wrongGetLock2(Jedis jedis, String lockKey, int expireTime) {

    long expires = System.currentTimeMillis() + expireTime;
    String expiresStr = String.valueOf(expires);

    // 如果当前锁不存在,返回加锁成功
    if (jedis.setnx(lockKey, expiresStr) == 1) {
        return true;
    }

    // 如果锁存在,获取锁的过期时间
    String currentValueStr = jedis.get(lockKey);
    if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
        // 锁已过期,获取上一个锁的过期时间,并设置现在锁的过期时间
        String oldValueStr = jedis.getSet(lockKey, expiresStr);
        if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
            // 考虑多线程并发的情况,只有一个线程的设置值和当前值相同,它才有权利加锁
            return true;
        }
    }
        
    // 其他情况,一律返回加锁失败
    return false;

}

Так в чем проблема с этим кодом? 1. Поскольку клиент сам генерирует время истечения, необходимо принудительно синхронизировать время каждого клиента в распределенной системе. 2. По истечении срока блокировки, если несколько клиентов выполняются одновременноjedis.getSet()метод, то, несмотря на то, что в конце может заблокироваться только один клиент, время истечения срока действия блокировки этого клиента может быть перезаписано другими клиентами. 3. Замок не имеет идентификатора владельца, то есть его может разблокировать любой клиент.

код разблокировки

правильная осанка

Или сначала покажите код, а потом возьмем вас объяснить, почему он реализован именно так:

public class RedisTool {

    private static final Long RELEASE_SUCCESS = 1L;

    /**
     * 释放分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @return 是否释放成功
     */
    public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {

        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));

        if (RELEASE_SUCCESS.equals(result)) {
            return true;
        }
        return false;

    }

}

Как видите, нам нужно всего две строки кода, чтобы разблокировать его! Первая строка кода, мы написали простой код сценария Lua Последний раз я видел этот язык программирования в «Хакеры и художники», но я не ожидал, что он будет использоваться в этот раз. Во второй строке кода мы передаем код Lua вjedis.eval()и назначьте параметр KEYS[1] для lockKey и ARGV[1] для requestId. Метод eval() передает код Lua серверу Redis для выполнения.

Итак, какова функция этого кода Lua? На самом деле это очень просто: сначала получаем значение, соответствующее блокировке, проверяем, совпадает ли оно с requestId, и удаляем блокировку (разблокировку), если она равна. Так зачем использовать язык Lua для его реализации? Потому что вышеуказанная операция гарантированно будет атомарной. О том, какие проблемы могут возникнуть с неатомарностью, можно прочитать[Код разблокировки — пример ошибки 2]. Итак, почему выполнение метода eval() может обеспечить атомарность благодаря характеристикам Redis.Ниже приводится частичное объяснение команды eval на официальном сайте:

Проще говоря, когда команда eval выполняет код Lua, код Lua будет выполняться как команда, и Redis не будет выполнять другие команды, пока не будет выполнена команда eval.

Пример ошибки 1

Самый распространенный код разблокировки — использовать напрямуюjedis.del()Метод удаляет блокировку Этот метод разблокировки блокировки без предварительного определения владельца блокировки приведет к тому, что любой клиент разблокирует ее в любое время, даже если блокировка не является его собственной.

public static void wrongReleaseLock1(Jedis jedis, String lockKey) {
    jedis.del(lockKey);
}

Пример ошибки 2

На первый взгляд такой код разблокировки не проблема.Даже я почти догадался раньше.Он похож на правильную позу.Только разница в том,что он разделен на две команды для выполнения.Код такой:

public static void wrongReleaseLock2(Jedis jedis, String lockKey, String requestId) {
        
    // 判断加锁与解锁是不是同一个客户端
    if (requestId.equals(jedis.get(lockKey))) {
        // 若在此时,这把锁突然不是这个客户端的,则会误解锁
        jedis.del(lockKey);
    }

}

Как и в комментариях к коду, проблема в том, что если вы вызоветеjedis.del()метод, когда блокировка больше не принадлежит текущему клиенту, блокировка, добавленная другими, будет снята. Так действительно ли существует такой сценарий? Ответ - да. Например, клиент А заблокирован, а клиент А разблокирован через определенный период времени.jedis.del()Раньше блокировка внезапно истекала.В это время клиент Б пытался успешно заблокировать, а затем клиент А выполнил метод del(), чтобы снять блокировку клиента Б.


Суммировать

В этой статье в основном рассказывается, как использовать код Java для правильной реализации распределенных блокировок Redis, а также приводятся два классических примера ошибок для блокировки и разблокировки. На самом деле реализовать распределенные блокировки через Redis несложно, если гарантируются четыре условия надежности. Хотя Интернет принес нам удобство, пока у нас есть вопрос, мы можем его погуглить, но должен ли ответ в Интернете быть правильным? На самом деле это не так, поэтому мы всегда должны сохранять дух вопрошания, больше думать и больше проверять.

Если Redis развернут на нескольких компьютерах в вашем проекте, вы можете попробовать использоватьRedissonРеализуйте распределенные блокировки, которые являются компонентами Java, официально предоставляемыми Redis, ссылка на которые приведена здесь.эталонное чтениеданы главы.


эталонное чтение

[1] Distributed locks with Redis

[2] EVAL command

[3] Redisson