предисловие
В повседневной разработке распределенные блокировки необходимы для бизнес-сценариев, таких как заказ за секунды, захват красных конвертов и т. д. А Redis очень подходит для использования в качестве распределенной блокировки. Эта статья будет разделена на семь планов, чтобы обсудить правильное использование распределенных блокировок Redis. Если что-то не так, пожалуйста, укажите на это, давайте учиться и добиваться прогресса вместе.
публика:маленький мальчик собирает улиток
-
Что такое распределенная блокировка
-
Вариант 1: SETNX + СРОЧНЫЙ СРОК
-
Вариант 2: SETNX + значение (системное время + время истечения)
-
Вариант 3: Используйте сценарий Lua (включая две инструкции SETNX + EXPIRE)
-
Вариант 4: Расширенная команда SET (SET EX PX NX)
-
Вариант 5: SET EX PX NX + проверить уникальное случайное значение, а затем снять блокировку
-
Вариант 6: Платформа с открытым исходным кодом: Redisson
-
Вариант 7: Распределенная блокировка Redlock, реализованная несколькими машинами
-
адрес github, спасибо за каждую звезду
Что такое распределенная блокировка
Распределенная блокировка на самом деле является реализацией блокировки, которая контролирует общий доступ различных процессов в распределенной системе к общим ресурсам. Если разные системы или разные хосты одной и той же системы совместно используют критический ресурс, часто требуется взаимное исключение, чтобы предотвратить взаимные помехи и обеспечить согласованность.
Давайте сначала посмотрим, какими характеристиками должна обладать надежная распределенная блокировка:
- взаимная исключительность: в любой момент только один клиент может удерживать блокировку.
- тайм-аут блокировки: блокировка удержания истекает, и ее можно снять, чтобы предотвратить ненужную трату ресурсов и предотвратить тупиковые ситуации.
- повторный вход: после того, как поток получит блокировку, он может снова запросить блокировку.
- Высокая производительность и высокая доступность: Накладные расходы на блокировку и разблокировку должны быть как можно меньше, и в то же время должна быть обеспечена высокая доступность, чтобы избежать отказа распределенной блокировки.
- безопасность: Блокировка может быть удалена только клиентом, который ее удерживает, но не другими клиентами.
Схема распределенной блокировки Redis 1: SETNX + EXPIRE
Когда речь заходит о распределенной блокировке Redis, многие мелкие партнеры сразу вспоминают об этом.setnx
+ expire
Заказ. то есть использовать сначалаsetnx
Приходите, чтобы схватить замок, если вы схватите его, используйте его сноваexpire
Установите время истечения срока действия, чтобы предотвратить снятие блокировки.
SETNX — это сокращение от SET IF NOT EXISTS. Ежедневный формат команды — это значение ключа SETNX. Если ключ не существует, SETNX успешно вернет 1, а если ключ уже существует, он вернет 0.
Предполагая, что продукт на веб-сайте электронной коммерции выполняет действие seckill, ключ может быть установлен на key_resource_id, а значение может быть установлено на любое значение.Псевдокод выглядит следующим образом:
if(jedis.setnx(key_resource_id,lock_value) == 1){ //加锁
expire(key_resource_id,100); //设置过期时间
try {
do something //业务请求
}catch(){
}
finally {
jedis.del(key_resource_id); //释放锁
}
}
Но в этой схемеsetnx
иexpire
Две команды разделены,не атомарная операция. Если законченоsetnx
Заблокировано, готовится к выполнениюexpire
Когда установлено время истечения, происходит сбой процесса или его необходимо перезапустить для обслуживания, тогда блокировка будет «бессмертной».Ни один другой поток никогда не сможет получить блокировку..
Схема распределенной блокировки Redis 2: SETNX + значение (системное время + срок действия)
Для решения один,Сценарии, в которых аварийные блокировки не могут быть сняты, некоторые друзья думают, что можно указать время истечения срока действияsetnx
внутри значения значения. Если блокировка не удалась, выньте значение и проверьте его снова. Код блокировки выглядит следующим образом:
long expires = System.currentTimeMillis() + expireTime; //系统时间+设置的过期时间
String expiresStr = String.valueOf(expires);
// 如果当前锁不存在,返回加锁成功
if (jedis.setnx(key_resource_id, expiresStr) == 1) {
return true;
}
// 如果锁已经存在,获取锁的过期时间
String currentValueStr = jedis.get(key_resource_id);
// 如果获取到的过期时间,小于系统当前时间,表示已经过期
if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
// 锁已过期,获取上一个锁的过期时间,并设置现在锁的过期时间(不了解redis的getSet命令的小伙伴,可以去官网看下哈)
String oldValueStr = jedis.getSet(key_resource_id, expiresStr);
if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
// 考虑多线程并发的情况,只有一个线程的设置值和当前值相同,它才可以加锁
return true;
}
}
//其他情况,均返回加锁失败
return false;
}
Преимущество этой схемы в том, что умное удалениеexpire
Для операции установки времени истечения отдельно поставьтеВремя истечения помещается в значение setnxЗаходи внутрь. Решена проблема, из-за которой блокировка не может быть снята при возникновении исключения в решении. Но у этого решения есть и другие недостатки:
- Время истечения генерируется самим клиентом (System.currentTimeMillis() — это время текущей системы) Необходимо, чтобы в распределенной среде время каждого клиента было синхронизировано.
- Если срок действия блокировки истек, и одновременно запрашивают несколько клиентов, все выполняют jedis.getSet(), и только один клиент может успешно заблокировать, но время истечения блокировки клиента может быть перезаписано другими клиентами.
- Блокировка не содержит уникального идентификатора владельца и может быть снята/разблокирована другими клиентами.
Схема распределенной блокировки Redis 3: используйте сценарий Lua (включая две инструкции SETNX + EXPIRE)
На самом деле, мы также можем использовать Lua-скрипт для обеспечения атомарности (включая две инструкции setnx и expire), lua-скрипт выглядит следующим образом:
if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then
redis.call('expire',KEYS[1],ARGV[2])
else
return 0
end;
Код блокировки выглядит следующим образом:
String lua_scripts = "if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then" +
" redis.call('expire',KEYS[1],ARGV[2]) return 1 else return 0 end";
Object result = jedis.eval(lua_scripts, Collections.singletonList(key_resource_id), Collections.singletonList(values));
//判断是否成功
return result.equals(1L);
У этого решения все же есть недостатки, над какими недостатками следует подумать в первую очередь. Вы также можете подумать об этом. По сравнению с вариантом 2, какой из них лучше?
Схема распределенной блокировки Redis, схема 4: расширенная команда SET (SET EX PX NX)
Помимо использования с помощью Lua-скриптов гарантируетсяSETNX + EXPIRE
Атомарность двух инструкций, мы также можем использовать инструкцию Redis SET для расширения параметров! (SET key value[EX seconds][PX milliseconds][NX|XX]
), он тоже атомный!
SET key value[EX seconds][PX milliseconds][NX|XX]
- NX: Указывает, что установка может быть успешной только в том случае, если ключ не существует, то есть гарантируется, что только первый запрос клиента может получить блокировку, а другие запросы клиента могут получить ее только после снятия блокировки.
- EX секунд : Установите время истечения срока действия ключа в секундах.
- PX миллисекунды: установите время истечения срока действия ключа в миллисекундах.
- XX: установить значение, только если ключ существует
Демонстрация псевдокода выглядит следующим образом:
if(jedis.set(key_resource_id, lock_value, "NX", "EX", 100s) == 1){ //加锁
try {
do something //业务处理
}catch(){
}
finally {
jedis.del(key_resource_id); //释放锁
}
}
Однако с этим решением все еще могут быть проблемы:
- Вопрос один:Блокировка просрочена и снята, а дело не выполнено. Предположим, что поток a успешно получает блокировку и выполняет код в критической секции. Но после того, как прошло 100 с, он не закончил выполнение. Однако срок действия блокировки в это время истек, и поток b снова запрашивает ее. Очевидно, поток b может успешно получить блокировку и начать выполнение кода в критической секции. Затем возникает проблема, бизнес-код в критической секции не выполняется строго последовательно.
- Вопрос второй:Блокировка была случайно удалена другим потоком. Предположим, что thread a завершает выполнение и освобождает блокировку. Но он не знает, что текущая блокировка может быть удержана потоком b (когда поток a уходит снимать блокировку, возможно, что время истечения срока действия истекло, и приходит поток b и занимает блокировку). Затем поток a освобождает блокировку потока b, но бизнес-код в критической секции потока b может быть еще не выполнен.
Вариант 5: SET EX PX NX + проверить уникальное случайное значение, а затем удалить
Так как блокировка может быть удалена другими потоками по ошибке, то в качестве значения мы устанавливаем случайное число, уникальное для текущего потока, при удалении проверяйте его, и все будет в порядке. Псевдокод выглядит следующим образом:
if(jedis.set(key_resource_id, uni_request_id, "NX", "EX", 100s) == 1){ //加锁
try {
do something //业务处理
}catch(){
}
finally {
//判断是不是当前线程加的锁,是才释放
if (uni_request_id.equals(jedis.get(key_resource_id))) {
jedis.del(lockKey); //释放锁
}
}
}
это здесь,Определить, является ли это блокировкой, добавленной текущим потокомиразблокировать замокНе атомарная операция. Если вы вызываете jedis.del() для снятия блокировки, блокировка может больше не принадлежать текущему клиенту, и блокировка, добавленная другими, будет снята.
Чтобы быть более строгим, вместо этого обычно используются сценарии lua. Lua-скрипт выглядит следующим образом:
if redis.call('get',KEYS[1]) == ARGV[1] then
return redis.call('del',KEYS[1])
else
return 0
end;
Схема распределенной блокировки Redis 6: платформа Redisson
Вариант 5 еще возможенБлокировка просрочена и освобождена, а дело не завершеноПроблема. Некоторые друзья считают, что достаточно установить срок действия блокировки немного дольше. В самом деле, давайте представим, можно ли запустить временный поток демона для потока, получившего блокировку, и проверять, существует ли блокировка через равные промежутки времени.Если она существует, время истечения блокировки будет продлено, чтобы предотвратить блокировки от истечения срока действия и освобождены заранее.
Нынешняя инфраструктура с открытым исходным кодом Redisson решает эту проблему. Давайте взглянем на базовую принципиальную схему Redisson:
Пока поток успешно заблокирован, он запуститwatch dog
Сторожевой таймер, который является фоновым потоком, будет проверять каждые 10 секунд. Если поток 1 все еще удерживает блокировку, он продолжит продлевать срок службы ключа блокировки. Следовательно, Redisson решается с помощью RedissonБлокировка просрочена и освобождена, а дело не завершенопроблема.
Схема распределенной блокировки Redis 7: распределенная блокировка Redlock+Redisson, реализованная несколькими машинами
Предыдущие шесть решений основаны только на обсуждении автономной версии, и они не идеальны. На самом деле Redis обычно развертывается в кластерах:
Если поток получает блокировку на главном узле Redis, но заблокированный ключ не был синхронизирован с подчиненным узлом. В это время главный узел выходит из строя, и подчиненный узел будет обновлен до главного узла. Второй поток может получить блокировку того же ключа, но первый поток уже получил блокировку, и безопасность блокировки будет потеряна.
Чтобы решить эту проблему, автор Redis Antirez предложил расширенный алгоритм распределенной блокировки: Redlock. Основная идея Redlock заключается в следующем:
Разверните несколько мастеров Redis, чтобы гарантировать, что они не отключатся одновременно. И эти мастер-ноды полностью независимы друг от друга, и между ними нет синхронизации данных. В то же время необходимо убедиться, что в этих нескольких главных экземплярах используется тот же метод для получения и снятия блокировок, что и в одном экземпляре Redis.
Мы предполагаем, что в настоящее время существует 5 главных узлов Redis, на которых запущены эти экземпляры Redis на 5 серверах.
Этапы внедрения RedLock следующие:
- 1. Получить текущее время в миллисекундах.
- 2. Запросите блокировки от 5 мастер-узлов последовательно. Клиент устанавливает время ожидания сетевого подключения и ответа, причем время ожидания должно быть меньше времени истечения блокировки. (Предполагая, что время автоматического отказа блокировки составляет 10 секунд, время ожидания обычно составляет от 5 до 50 миллисекунд. Предположим, что время ожидания составляет 50 мс). Если время ожидания истекло, пропустите главный узел и попробуйте следующий главный узел как можно скорее.
- 3. Клиент использует текущее время минус время начала получения блокировки (то есть время, записанное на шаге 1), чтобы получить время, использованное для получения блокировки. Если и только если более половины (N/2+1, здесь 5/2+1=3 узлов) главных узлов Redis получили блокировку, а время использования меньше времени истечения срока действия блокировки, блокировка будет снята. быть успешно приобретенным. (Как показано выше, 10 с> 30 мс + 40 мс + 50 мс + 4 мс + 50 мс)
- Если блокировка получена, реальное время действия ключа изменится, и время, использованное для получения блокировки, необходимо вычесть.
- Если блокировка не может быть получена (блокировка не была получена по крайней мере в N/2+1 мастер-экземплярах, или время получения блокировки превысило действительное время), клиенту необходимо разблокировать все мастер-узлы (даже если некоторые на мастер-узлах его вообще нет. Если блокировка прошла успешно, ее также нужно разблокировать, чтобы предотвратить какое-то проскальзывание по сети).
Следующие шаги упрощены:
- Запросить блокировки от 5 мастер-узлов последовательно
- В соответствии с установленным временем ожидания решается, следует ли пропустить главный узел.
- Если более или равно трем узлам успешно заблокировано, а использованное время меньше периода действия блокировки, можно определить, что блокировка прошла успешно.
- Если он не может получить замок, разблокируйте его!
Redisson реализует версию замка redLock, если вам интересно, вы можете узнать об этом~
публика
- Добро пожаловать в общедоступный номер: Маленький мальчик, который подобрал улитку