Во-первых, принцип реализации
1.1 Основные принципы
Собственные блокировки JDK допускают различныенитьДоступ к общим ресурсам взаимоисключающим образом, но если вы хотитепроцессЕсли доступ к общим ресурсам осуществляется взаимоисключающим образом, собственная блокировка JDK бессильна. На этом этапе Redis можно использовать для реализации распределенных блокировок.
Основные команды Redis для реализации распределенных блокировок следующие:
SETNX key value
Функция команды SETNX: если указанный ключ не существует, создать и установить для него значение, а затем вернуть код состояния 1; если указанный ключ существует, вернуть 0 напрямую. Если возвращаемое значение равно 1, это означает, что блокировка получена, когда другие процессы попытаются создать ее снова, поскольку ключ уже существует, он вернет 0, что означает, что блокировка занята.
Когда процесс, получивший блокировку, завершает работу, он передаетdel
Команда удаляет ключ, и другие процессы могут конкурировать за создание и повторное получение блокировки.
Обычно во избежание взаимоблокировки мы устанавливаем тайм-аут для блокировки, который можно передать в Redis.expire
команда для реализации:
EXPIRE key seconds
Здесь мы объединяем их и используем клиент Jedis для реализации кода следующим образом:
Long result = jedis.setnx("lockKey", "lockValue");
if (result == 1) {
// 如果此处程序被异常终止(如直接kill -9进程),则设置超时的操作就无法进行,该锁就会出现死锁
jedis.expire("lockKey", 3);
}
Приведенный выше код имеет атомарную проблему, то есть операция setnx + expire не является атомарной.Если программа аварийно завершится до того, как будет установлен период ожидания, программа заблокируется. На этом этапе команды SETNX и EXPIRE можно записать в одном и том же сценарии Lua, а затем, вызвав команду Jediseval()
метода для выполнения, а Redis гарантирует атомарность всей операции Lua-скрипта. Этот метод громоздок в реализации, поэтому в официальной документации рекомендуется другой, более элегантный способ реализации:
1.2 Официальная рекомендация
[Официальный документ] (Распределенные блокировки с Redis) Рекомендуется использовать команду set напрямую для реализации:
SET key value [EX seconds|PX milliseconds] [NX|XX] [KEEPTTL]
Здесь мы в основном фокусируемся на следующих четырех параметрах:
- EX: Установите время таймаута в секундах;
- PX: Установите время ожидания в миллисекундах;
- NX: установить тогда и только тогда, когда соответствующий ключ не существует;
- XX: Установите, если и только если соответствующий ключ существует.
Эти четыре параметра поддерживаются, начиная с версии Redis 2.6.12.Поскольку большинство текущих используемых Redis старше этой версии, рекомендуется использовать эту команду напрямую для реализации распределенных блокировок. Соответствующий код джедаев выглядит следующим образом:
jedis.set("lockKey", "lockValue", SetParams.setParams().nx().ex(3));
На этом этапе одна команда может завершить настройку значения и периода ожидания, а поскольку существует только одна команда, ее атомарность также гарантируется. Но из-за введения тайм-аута во избежание взаимоблокировки это также приводит к двум другим проблемам:
- Вопрос первый: когда время обработки бизнес-процессов превышает время истечения срока действия (процесс A на рисунке), поскольку блокировка была снята, другие процессы могут получить блокировку (процесс B на рисунке), что означает, что есть два процесса (A и B). ) одновременно входит в критическую секцию, и распределенная блокировка в это время становится недействительной;
- Вопрос второй: Как показано на рисунке выше, когда бизнес-обработка процесса A завершена, блокировка процесса B в это время удаляется, что, в свою очередь, снова приводит к сбою распределенной блокировки, позволяя процессу B и процессу C войти в критический режим. площадь одновременно.
Для задачи 2 мы можем присвоить замку уникальный идентификатор в качестве значения ключа при создании замка, здесь мы предполагаем, что используемUUID + 线程ID
как уникальный идентификатор:
String identifier = UUID.randomUUID() + ":" + Thread.currentThread().getId();
jedis.set("LockKey", identifier, SetParams.setParams().nx().ex(3));
Затем перед удалением блокировки уникальный идентификатор сравнивается со значением блокировки, если они не равны, то это свидетельствует о том, что блокировка не принадлежит текущему объекту операции, и операция удаления в это время не выполняется. Чтобы обеспечить общую атомарность операции оценки и операции удаления, здесь необходимо использовать сценарий Lua:
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
Этот сценарий означает, что если значение value совпадает с заданным значением, выполнить команду удаления, в противном случае напрямую вернуть код состояния 0. Соответствующий код, реализованный с помощью Jedis, выглядит следующим образом:
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
jedis.eval(script,
Collections.singletonList("LockKey"), // keys的集合
Collections.singletonList(identifier) // args的集合
);
Затем посмотрите на проблему 1. Самое простое решение проблемы 1: вы можете оценить максимальное время обработки бизнеса, а затем убедиться, что установленное время истечения срока больше, чем максимальное время обработки. Однако, поскольку бизнес будет сталкиваться с различными сложными ситуациями, не может быть гарантировано, что бизнес может быть обработан в течение указанного времени истечения каждый раз, В этом случае можно использовать стратегию продления периода блокировки.
1.3 Продлить период блокировки
Схема продления срока действия блокировки следующая: Предполагая, что тайм-аут блокировки составляет 30 секунд, программа должна регулярно проверять, существует ли еще блокировка.Время сканирования должно быть меньше периода тайм-аута, который обычно может быть установить на 1/3 периода тайм-аута.В этом случае это сканирование каждые 10 секунд. Если блокировка все еще существует, сбросьте ее время ожидания до 30 секунд. По этой схеме, пока бизнес не будет обработан, блокировка останется в силе, и как только бизнес будет обработан, программа немедленно удалит блокировку.
Распределенная блокировка, предоставляемая Java-клиентом Redis, Redisson, поддерживает аналогичную стратегию увеличения продолжительности блокировки, называемую WatchDog, что буквально переводится как механизм «сторожевой таймер».
Вышеприведенное обсуждение касается распределенных блокировок Redis в автономной среде. Чтобы обеспечить высокую доступность распределенных блокировок Redis, прежде всего, Redis должен быть высокодоступным. В основном существует два режима высокой доступности Redis: сигнальный режим и режим кластера. . Отдельно обсуждаются:
2. Sentinel Mode и распределенные блокировки
Режим Sentinel — это обновленная версия режима master-slave, который может автоматически переключаться при сбое и выбирать новый главный узел. Однако, поскольку механизм репликации Redis является асинхронным, распределенная блокировка, реализованная в режиме Sentinel, ненадежна по следующим причинам:
- Поскольку операция репликации между ведущим и подчиненным является асинхронной, когда блокировка создается на главном узле, блокировка на подчиненном узле в это время может не создаваться. Если главный узел в это время не работает, распределенная блокировка не будет создана на подчиненном узле;
- После того, как подчиненный узел становится главным узлом, другие процессы (или потоки) все еще могут создавать распределенные блокировки на новом главном узле.В это время несколько процессов (или потоков) одновременно входят в критическую секцию, и распределенный блокировка становится недействительной.
Поэтому в дозорном режиме не избежать проблемы сбоя блокировки. Поэтому, если вы хотите добиться высокой доступности распределенных блокировок, вы можете взять другое решение высокой доступности Redis — режим кластера Redis.
3. Кластерный режим и распределенные блокировки
3.1 Решение RedLock
Для реализации распределенных блокировок в кластерном режиме Redis предоставляет решение под названием RedLock. Предположим, у нас есть экземпляры Redis N. В это время процесс выполнения клиента выглядит следующим образом:
- Запишите текущее время в миллисекундах в качестве времени начала;
- Затем так же, как и в автономной версии, попробуйте создать блокировку на каждом экземпляре по очереди. Чтобы избежать блокировки, вызванной длительным взаимодействием клиента с неисправным узлом Redis, здесь используется метод быстрого опроса: если предположить, что время ожидания, установленное при создании блокировки, составляет 10 секунд, время ожидания для доступа к каждому Экземпляр Redis может быть секунд 5. Между 50 миллисекундами, если связь не была установлена в течение этого времени, попробуйте подключиться к следующему экземпляру;
- Если блокировка была успешно создана как минимум на N/2+1 экземплярах. а также
当前时间 - 开始时间 < 锁的超时时间
, блокировка считается полученной, а время действия блокировки равно超时时间 - 花费时间
(Если учесть дрейф часов серверов, на которых расположены разные экземпляры Redis, вам также необходимо вычесть дрейф часов); - Если экземпляров меньше N/2+1, считается, что создание распределенных блокировок не удалось, и созданные блокировки на этих экземплярах необходимо удалить, чтобы их могли создать другие клиенты.
- После сбоя клиента он может подождать произвольное количество времени перед повторной попыткой.
Выше приведена схема реализации RedLock, видно, что она в основном реализуется клиентом и на самом деле не задействует функции, связанные с кластером Redis. Таким образом, N экземпляров Redis здесь не обязательно должны быть настоящим кластером Redis, они могут быть полностью независимы друг от друга, но поскольку для получения блокировки требуется получить блокировку только половине узлов, он по-прежнему обладает отказоустойчивостью и высокой наличие . Это будет проверено позже, когда мы будем использовать Redisson для демонстрации RedLock.
3.2 Связь с малой задержкой
Кроме того, когда клиент, реализующий схему RedLock, взаимодействует со всеми экземплярами Redis, он должен обеспечивать низкую задержку, и лучше всего использовать технологию мультиплексирования, чтобы гарантировать, что команда SET отправляется на все узлы Redis одновременно, а соответствующие результаты принадлежащий. Если сетевая задержка высока, при условии, что оба клиента A и B пытаются создать блокировки:
SET key 随机数A EX 3 NX #A客户端
SET key 随机数B EX 3 NX #B客户端
В этот момент возможно, что клиент A создал блокировку на половине узлов, а клиент B создал блокировку на другой половине узлов, тогда ни один из клиентов не сможет получить блокировку. Если параллелизм высокий, может быть несколько клиентов, создающих блокировки на некоторых узлах, при этом ни один клиент не превысит N/2+1. Это последний шаг вышеописанного процесса.Подчеркивается, что после сбоя клиента ему необходимо подождать случайное время перед повторной попыткой.Если это фиксированное время, все неудачные клиенты будут повторять попытку одновременно. .
Поэтому наилучшей реализацией является то, что клиентская команда SET может достигать всех узлов практически одновременно и получать все результаты выполнения практически одновременно. Чтобы обеспечить это, чрезвычайно важна сетевая связь с малой задержкой.Redisson, описанный ниже, использует структуру Netty для обеспечения реализации этой функции.
3.3 Постоянство и высокая доступность
Чтобы обеспечить высокую доступность, все узлы Redis также должны включить постоянство. Предполагая, что постоянство не включено, предполагая, что процесс A обрабатывает бизнес-логику после получения блокировки, узел перезапускается, когда узел не работает, поскольку данные блокировки потеряны, другие процессы могут снова создать блокировку, поэтому все узлы Redis должны чтобы включить сохранение AOF Way.
Механизм синхронизации AOF по умолчанию:everysec
, то есть процесс сохраняется один раз в секунду. В это время могут быть приняты во внимание как производительность, так и безопасность данных. При непредвиденном простое данные будут потеряны не более чем на одну секунду. Но если случится так, что процесс А создаст блокировку в течение этой секунды, и данные будут потеряны из-за простоя. В это время другие процессы также могут создать блокировку, и взаимное исключение блокировки недействительно. Есть два способа решить эту проблему:
-
метод первый: Измените Redis.conf в
appendfsync
ценностьalways
, то есть персистентность выполняется после каждой команды, что снизит производительность Redis, что также снизит производительность распределенных блокировок, но абсолютно гарантировано взаимное исключение блокировок; - Способ 2: как только узел не работает, ему необходимо дождаться истечения периода ожидания блокировки перед перезапуском, что эквивалентно естественному отказу исходной блокировки (но сначала необходимо убедиться, что бизнес может быть завершен в течение установленного периода времени ожидания). Эта схема также известна как отложенный перезапуск.
4. Редиссон
Redisson — это Java-клиент Redis, который предоставляет различные реализации распределенных блокировок Redis, такие как повторные блокировки, справедливые блокировки, RedLock, блокировки чтения-записи и т. д. Использование в производственной среде.
4.1 Распределенная блокировка
Использовать Redisson для создания автономной версии распределенного замка очень просто, пример следующий:
// 1.创建RedissonClient,如果与spring集成,可以将RedissonClient声明为Bean,在使用时注入即可
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.0.100:6379");
RedissonClient redissonClient = Redisson.create(config);
// 2.创建锁实例
RLock lock = redissonClient.getLock("myLock");
try {
//3.尝试获取分布式锁,第一个参数为等待时间,第二个参数为锁过期时间
boolean isLock = lock.tryLock(10, 30, TimeUnit.SECONDS);
if (isLock) {
// 4.模拟业务处理
System.out.println("处理业务逻辑");
Thread.sleep(20 * 1000);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//5.释放锁
lock.unlock();
}
redissonClient.shutdown();
На данный момент соответствующая структура данных в Redis выглядит следующим образом:
Вы можете видеть, что ключ — это имя замка, заданное в коде, а тип значения — хеш, где ключ9280e909-c86b-43ec-b11d-6e5a7745e2e9:13
ФорматUUID + 线程ID
; Значение, соответствующее ключу, равно 1, что представляет собой количество блокировок. Причина, по которой используется формат хеша, в основном заключается в том, что блокировки, созданные Redisson, являются реентерабельными, то есть вы можете блокировать несколько раз:
boolean isLock1 = lock.tryLock(0, 30, TimeUnit.SECONDS);
boolean isLock2 = lock.tryLock(0, 30, TimeUnit.SECONDS);
В этот момент соответствующее значение станет равным 2, что означает добавление двух замков:
Конечно, как и в случае с другими реентерабельными блокировками, необходимо следить за тем, чтобы количество разблокировок было таким же, как и количество полностью разблокируемых блокировок:
lock.unlock();
lock.unlock();
4.2 RedLock
Redisson также реализует решение RedLock, официально рекомендованное Redis.Здесь мы запускаем три экземпляра Redis для демонстрации.Они могут быть полностью независимы друг от друга и не требуют конфигурации, связанной с кластером:
$ ./redis-server ../redis.conf
$ ./redis-server ../redis.conf --port 6380
$ ./redis-server ../redis.conf --port 6381
Соответствующий пример кода выглядит следующим образом:
// 1.创建RedissonClient
Config config01 = new Config();
config01.useSingleServer().setAddress("redis://192.168.0.100:6379");
RedissonClient redissonClient01 = Redisson.create(config01);
Config config02 = new Config();
config02.useSingleServer().setAddress("redis://192.168.0.100:6380");
RedissonClient redissonClient02 = Redisson.create(config02);
Config config03 = new Config();
config03.useSingleServer().setAddress("redis://192.168.0.100:6381");
RedissonClient redissonClient03 = Redisson.create(config03);
// 2.创建锁实例
String lockName = "myLock";
RLock lock01 = redissonClient01.getLock(lockName);
RLock lock02 = redissonClient02.getLock(lockName);
RLock lock03 = redissonClient03.getLock(lockName);
// 3. 创建 RedissonRedLock
RedissonRedLock redLock = new RedissonRedLock(lock01, lock02, lock03);
try {
boolean isLock = redLock.tryLock(10, 300, TimeUnit.SECONDS);
if (isLock) {
// 4.模拟业务处理
System.out.println("处理业务逻辑");
Thread.sleep(200 * 1000);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//5.释放锁
redLock.unlock();
}
redissonClient01.shutdown();
redissonClient02.shutdown();
redissonClient03.shutdown();
В настоящее время каждый экземпляр Redis заблокирован следующим образом:
Вы можете видеть, что блокировка устанавливается для каждого экземпляра.
4.3 Продлить период блокировки
Наконец, давайте представим механизм Redisson WatchDog, который можно использовать для увеличения продолжительности блокировки. Примеры следующие:
Config config = new Config();
// 1.设置WatchdogTimeout
config.setLockWatchdogTimeout(30 * 1000);
config.useSingleServer().setAddress("redis://192.168.0.100:6379");
RedissonClient redissonClient = Redisson.create(config);
// 2.创建锁实例
RLock lock = redissonClient.getLock("myLock");
try {
//3.尝试获取分布式锁,第一个参数为等待时间
boolean isLock = lock.tryLock(0, TimeUnit.SECONDS);
if (isLock) {
// 4.模拟业务处理
System.out.println("处理业务逻辑");
Thread.sleep(60 * 1000);
System.out.println("锁剩余的生存时间:" + lock.remainTimeToLive());
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//5.释放锁
lock.unlock();
}
redissonClient.shutdown();
Прежде всего, механизм Redisson WatchDog будет действовать только на те блокировки, для которых не установлено время ожидания блокировки, поэтому здесь мы вызываем с двумя параметрами.tryLock()
метод:
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
вместо трех параметров, содержащих тайм-аутtryLock()
метод:
boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException;
Во-вторых, мы проходимconfig.setLockWatchdogTimeout(30 * 1000)
Установите значение lockWatchdogTimeout на 30 000 миллисекунд (по умолчанию также 30 000 миллисекунд). В это время механизм Redisson WatchDog будет проверять все блокировки без периода тайм-аута с 1/3 периода lockWatchdogTimeout (10 секунд в данном случае), если дело не было обработано (т.е. блокировка не была активно удалена программой) , Redisson сбрасывает тайм-аут блокировки на значение, указанное в lockWatchdogTimeout (здесь 30 секунд), пока программа не удалит блокировку. Следовательно, в приведенном выше примере видно, что независимо от того, как долго установлено время ожидания имитируемой службы, блокировка будет иметь определенное оставшееся время жизни, пока обработка службы не будет завершена.
Наоборот, если явно задано время ожидания блокировки, арендное время, то время арендного времени имеет преимущественную силу, потому что механизм WatchDog не будет действовать для блокировок с явно указанным периодом ожидания.
использованная литература
Дополнительные статьи см. в [Full Stack Engineer Manual], адрес GitHub:GitHub.com/Black and WhiteShould/…