1 В избранное 0 Ответвления 0

OSCHINA-MIRROR/yangdechao_admin-guage-notes

В этом репозитории не указан файл с открытой лицензией (LICENSE). При использовании обратитесь к конкретному описанию проекта и его зависимостям в коде.
Клонировать/Скачать
06Redis实现分布式锁的7种方案及正确使用姿势.md 24 КБ
Копировать Редактировать Web IDE Исходные данные Просмотреть построчно История
Отправлено 24.06.2025 02:12 0782333

Введение в схемы

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

  • Что такое распределённая блокировка
  • Схема 1: SETNX + EXPIRE
  • Схема 2: SETNX + значение поля (текущее время + время истечения)
  • Схема 3: Использование скрипта Lua (включающего SETNX + EXPIRE)
  • Схема 4: Расширенная команда SET (SET EX PX NX)
  • Схема 5: SET EX PX NX + проверка уникального случайного значения перед освобождением блокировки
  • Схема 6: Открытый фреймворк Redisson
  • Схема 7: Распределённая блокировка Redlock для нескольких машин

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

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

❞Давайте рассмотрим основные характеристики надёжной распределённой блокировки:

Изображение

  • «Взаимоисключение»: В любой момент времени только один клиент может владеть блокировкой.
  • «Истечение времени блокировки»: Если владелец блокировки не освобождает её в течение определённого времени, она автоматически освобождается, что предотвращает потерю ресурсов и возможность возникновения мёртвых замков.
  • «Перезахват блокировки»: Если клиент уже владеет блокировкой, он может снова её захватить.
  • «Высокая производительность и надёжность»: Операции захвата и освобождения блокировки должны быть максимально быстрыми и надёжными, чтобы избежать отказа распределённой блокировки.
  • «Безопасность»: Блокировка может быть освобождена только её владельцем, а не другими клиентами.

Схема 1 распределённой блокировки Redis: SETNX + EXPIRE

Когда речь заходит о распределённых блокировках Redis, многие сразу вспоминают команды SETNX и EXPIRE. Это означает, что сначала используется SETNX, чтобы захватить блокировку, а затем EXPIRE для установки времени жизни блокировки, чтобы предотвратить забывание её освобождения.>

Команда SETNX является сокращением от SET IF NOT EXISTS. Её формат команды — SETNX key value. Если ключ не существует, команда успешно выполнится и вернёт 1. Если ключ уже существует, команда вернёт 0.

Предположим, что интернет-магазин проводит акцию моментальных покупок для определённого товара. Ключ можно установить как key_resource_id, а значение — любое. Пример псевдокода представлен ниже:

if (jedis.setnx(key_resource_id, lock_value) == 1) { // закрепление
    expire(key_resource_id, 100); // установка времени жизни
    try {
        делай что-то  // бизнес-запрос
    } catch () {
    }
    finally {
        jedis.del(key_resource_id); // освобождение закрепления
    }
}

Однако в этом решении команды setnx и expire разделены, «что не является атомарной операцией». Если после выполнения setnx происходит аварийное завершение процесса или требуется перезапуск для обслуживания, то закрепление становится «вечным», «и другие потоки никогда не смогут получить это закрепление».

Второе решение Redis для распределенного закрепления: SETNX + значение value равно (текущее время системы + время жизни)

Чтобы решить проблему первого решения, когда «при возникновении ошибки закрепление не освобождается», некоторые участники предложили включить время жизни в значение setnx. Если закрепление не удалось, можно извлечь значение value для проверки. Код для закрепления выглядит следующим образом:

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()) {

    // Закрепление просрочено, получить старое время жизни и установить новое время жизни (если вы не знакомы с командой getSet Redis, вы можете найти информацию на официальном сайте)
    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 три: использование скриптов 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);

Эта схема, сравнивая её со второй схемой, какую из них вы считаете лучше?

Схема распределённой блокировки Redis четыре: расширенные команды для SET (SET EX PX NX)

Кроме использования скриптов Lua для обеспечения атомарности двух команд SETNX и EXPIRE, мы также можем использовать расширенные параметры команды SET в Redis! (SET key value[EX seconds][PX milliseconds][NX|XX]) Это тоже является атомарной операцией!

SET key value[EX seconds][PX milliseconds][NX|XX]

  • NX: Устанавливает значение только если ключ отсутствует, то есть гарантирует, что только первый клиент может получить блокировку, а остальные должны ждать освобождения блокировки.
  • EX seconds: Устанавливает время жизни ключа в секундах.
  • PX milliseconds: Устанавливает время жизни ключа в миллисекундах.
  • XX: Устанавливает значение только если ключ существует.

Таким образом, использование расширенных параметров команды SET позволяет выполнить атомарную операцию без необходимости писать скрипт Lua.Пример псевдокода:

if (jedis.set(key_resource_id, lock_value, "NX", "EX", 100) == 1) { // установка блокировки
    try {
        // выполнение бизнес-логики
    } catch (Exception e) {
        // обработка ошибок
    } finally {
        jedis.del(key_resource_id); // освобождение блокировки
    }
}

Однако, эта схема всё ещё может иметь проблемы:

  • Проблема 1: "Срок действия блока истёк, но бизнес-логика ещё не завершена". Предположим, что поток a успешно захватил блокировку и продолжает выполнять код критической секции. Однако спустя 100 секунд он всё ещё не завершил выполнение. В это время срок действия блокировки истёк, и поток b запросил блокировку. Очевидно, что поток b сможет успешно получить блокировку и начать выполнять код критической секции. Таким образом, бизнес-логика критической секции уже не выполняется строго последовательно.

  • Проблема 2: "Блокировка была случайно удалена другим потоком". Предположим, что после завершения работы поток 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 {
        выполнить какое-то действие  // бизнес-логика
    } 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;

Схема 6 Redis-распределенной блокировки с использованием Redisson-фреймворкаСхема 5 все еще может иметь проблему "освобождение блока по истечению времени, когда бизнес-логика ещё не завершена". Некоторые участники считают, что можно просто увеличить время жизни блока. Однако можно также запустить фоновый поток, который будет периодически проверять наличие блока и продлевать его время жизни, если он всё ещё используется.Открытый фреймворк Redisson решает эту проблему. Давайте рассмотрим схему работы Redisson:

Изображение

Как только поток успешно захватывает блок, запускается фоновый поток watch dog, который каждые 10 секунд проверяет наличие блока. Если поток всё ещё использует блок, время его жизни продлевается. Таким образом, Redisson решает проблему "освобождение блока по истечению времени, когда бизнес-логика ещё не завершена".

Схема 7: Распределённый блокировщик Redlock + Redisson для нескольких машин

Первые шесть схем обсуждались в контексте одного сервера, что ещё не идеально. На самом деле Redis обычно развернут в виде кластера:Изображение

Если поток 1 получил блокировку на мастер-узле Redis, но ключ ещё не синхронизирован на slave-узел, и в этот момент мастер-узел выходит из строя, один из slave-узлов становится новым мастером. В этом случае поток 2 может получить блокировку для того же ключа, хотя блокировка уже была получена потоком 1, что нарушает целостность блокировки.

Чтобы решить эту проблему, автор Redis Антриз предложил продвинутый алгоритм распределённой блокировки Redlock. Основная идея Redlock заключается в следующем:> ❝

Создайте несколько мастер-узлов Redis, чтобы гарантировать, что они не будут одновременно выходить из строя. Эти мастер-узлы должны быть полностью независимыми друг от друга и не должны синхронизировать данные между собой. При этом необходимо обеспечить, что метод получения и освобождения блокировок будет таким же, как при использовании одного экземпляра Redis.

Предположим, что у нас есть пять мастер-узлов Redis, работающих на пяти разных серверах.

Изображение

Шаги реализации Redlock:> ❝

    1. Получите текущее время в миллисекундах.
    1. Последовательно запросите блокировку на пяти мастер-узлах. Клиент должен установить сетевые соединения и время ожидания ответа, которое должно быть меньше времени жизни блокировки (например, если время автоматического истечения блокировки составляет 10 секунд, то время ожидания обычно составляет 5-50 миллисекунд, предположим, что оно равно 50 мс). Если время ожидания истекло, пропустите этот мастер-узел и как можно скорее попытайтесь получить блокировку на следующем мастер-узле.
    1. Клиент использует текущее время минус время начала запроса блокировки (то есть время, записанное в шаге 1), чтобы получить время, затраченное на получение блокировки. Блокировка считается успешно полученной, если более половины мастер-узлов (N/2+1, здесь это 5/2+1=3 узла) предоставили блокировку, и время, затраченное на получение блокировки, меньше времени жизни блокировки (как показано на рисунке, 10 с > 30 мс + 40 мс + 50 мс + 4 мс + 50 мс).
  • Если блокировка была получена, реальное время действия ключа изменится, нужно вычесть время, затраченное на получение блокировки.> - Если блокировка не была получена (не удалось получить блокировку на хотя бы N/2+1 мастер-узлах, или время получения блockировки превысило время жизни блокировки), клиент должен освободить блокировку на всех мастер-узлах (даже если некоторые мастер-узлы не смогли получить блокировку, их тоже следует освободить, чтобы избежать утечек).

❞Упрощённые шаги следуют ниже:- Последовательно запрашивать блокировку у 5 мастер-узлов.

  • Определять, следует ли пропустить данный мастер-узел, в зависимости от установленного времени ожидания.
  • Если блокировка успешно установлена на 3 или более узлах, и время установки меньше срока действия блокировки, то блокировка считается установленной.
  • В случае неудачи установки блокировки, освобождать её.

Redisson реализует версию redlock блокировки.

Опубликовать ( 0 )

Вы можете оставить комментарий после Вход в систему

1
https://api.gitlife.ru/oschina-mirror/yangdechao_admin-guage-notes.git
git@api.gitlife.ru:oschina-mirror/yangdechao_admin-guage-notes.git
oschina-mirror
yangdechao_admin-guage-notes
yangdechao_admin-guage-notes
master