Семь вариантов! Обсудить правильное использование распределенной блокировки Redis.

Java Redis

предисловие

В повседневной разработке распределенные блокировки необходимы для бизнес-сценариев, таких как заказ за секунды, захват красных конвертов и т. д. А 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, спасибо за каждую звезду

GitHub.com/Я бы хотел 123/Java…

Что такое распределенная блокировка

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

Давайте сначала посмотрим, какими характеристиками должна обладать надежная распределенная блокировка:

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

Схема распределенной блокировки 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, если вам интересно, вы можете узнать об этом~

публика

  • Добро пожаловать в общедоступный номер: Маленький мальчик, который подобрал улитку

Ссылка и спасибо