Внедрить несколько распространенных методов записи распределенных блокировок.

Redis

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

Всем привет, меня зовут jack xu, сегодня я расскажу вам о распределенных блокировках. Прежде всего, давайте поговорим о том, что такое распределенная блокировка.Когда мы находимся в бизнес-сценариях, таких как размещение заказов на сокращение запасов, захват билетов, выбор курсов и захват красных конвертов, если здесь нет контроля блокировки, это вызовет серьезные проблемы. Ребята, которые изучили многопоточность, знают, что для предотвращения одновременного выполнения нескольких потоков одного и того же фрагмента кода мы можем использовать ключевое слово synchronized или класс ReentrantLock в JUC для управления, но в настоящее время почти любая система развертывает несколько машин, на одной машине мало развернутых приложений, а синхронизация и ReentrantLock не могут играть никакой роли.В настоящее время глобальная блокировка необходима для замены синхронизированной и ReentrantLock в JAVA.

Существует три популярных реализации распределенных блокировок, а именно реализация на основе кеша Redis, реализация на основе временных последовательных узлов zk и реализация на основе блокировок строк базы данных. Давайте сначала создадим этот замок с помощью команды setnx в Jedis.

Исходный код этой статьи находится по адресу:GitHub.com/Сюй Хаоцзя/Горячая земля…

джедаи пишут

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

    /**
     * 尝试获取分布式锁
     *
     * @param jedis      Redis客户端
     * @param lockKey    锁
     * @param requestId  请求标识
     * @param expireTime 超期时间
     * @return 是否获取成功
     */
    public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
        // set支持多个参数 NX(not exist) XX(exist) EX(seconds) PX(million seconds)
        String result = jedis.set(lockKey, requestId, "NX", "EX", expireTime);
        if (LOCK_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }

Этот код очень прост, в основном для того, чтобы сказать, что используемая здесь команда представляет собой значение ключа SET [EX секунд|PX миллисекунд] [NX|XX] [KEEPTTL], а команда SETNX+EXPIRE не используется, потому что SETNX+EXPIRE две команды не могут гарантировать атомарность, в то время как SETатомработать. Так зачем устанавливать здесь тайм-аут? Причина в том, что когда клиент получает блокировку и зависает в процессе выполнения задачи, а явным образом снять блокировку уже поздно, этот ресурс будет заблокирован навсегда, что приведет к взаимоблокировке, поэтому необходимо установитьсверхурочное время.

Код для снятия блокировки выглядит следующим образом:

    /**
     * 释放分布式锁
     *
     * @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;
    }

Здесь также следует отметить два момента: первыйПроблема должна закончиться, как вы понимаете, то есть блокировку B, добавленную A, нельзя удалить, иначе будет полный бардак, кто добавил блокировку, тот ее и разблокирует. Thread.currentThread().getId(), а затем определить, является ли это текущим потоком при удалении. Второй момент заключается в том, что проверка и снятие блокировки — это две независимые операции, а неатомСекс, как это решить? Используя скрипт Lua, т.е. если redis.call('get', KEYS[1]) == ARGV[1], то returnredis.call('del', KEYS[1]), иначе возвращает 0 end, это дает нам гарантированный атомарный секс .

Исходный код находится по адресу: com/xhj/distributedLock/DistLock.java.

Редиссон пишет

Redisson является одним из клиентов Redis для Java и предоставляет некоторые API для облегчения операций Redis. Но клиент Redisson немного мощнее, давайте сначала откроем официальный сайт.GitHub.com/Редис сын/Горячие… В этом каталоге много функций.Redisson отличается от Jedis позиционированием.Это не простой клиент Redis, а распределенный сервис на основе Redis.Мы видим, что в пакете JUC также есть имена классов.Redisson помогает нам, если у вас есть распределенная версия, такая как AtomicLong, вы можете напрямую использовать RedissonAtomicLong. Замок — это только верхушка айсберга, и он поддерживает все режимы, такие как ведущий-ведомый, дозорный, кластерный и т. д. Конечно, режим с одним узлом определенно поддерживается.

В Redisson предусмотрена более простая реализация распределенной блокировки.Давайте посмотрим на ее использование.Она довольно проста.Его можно сделать в две строки кода,что намного проще,чем в Jedis,и проблемы,которые нужно учитывать в Jedis уже помогите нам закончить.

Давайте посмотрим. Есть много способов получить блокировки, в том числе честные блокировки и блокировки чтения-записи. Мы используем redissonClient.getLock, который является реентерабельной блокировкой.

Теперь я запускаю программу

Откройте инструмент Redis Desktop Manager и посмотрите, что он хранит. Получается, что при блокировке записывается значение типа HASH, ключ имя блокировки jackxu, поле имя потока, а значение равно 1 (то есть количество повторных входов блокировки ).

Друзья могут подумать, что я несу чушь, не беда, давайте кликнем и посмотрим, действительно ли реализован его исходный код.

Нажмите на метод tryAcquire() метода tryLock(), затем перейдите в ->tryAcquireAsync(), а затем в ->tryLockInnerAsync() и, наконец, увидите истинное лицо горы Лу. Луа скрипт.

Сейчас вытаскиваю этот Lua-скрипт и анализирую, очень просто.

// KEYS[1] 锁名称 updateAccount
// ARGV[1] key 过期时间 10000ms
// ARGV[2] 线程名称
// 锁名称不存在
if (redis.call('exists', KEYS[1]) == 0) then
// 创建一个 hash,key=锁名称,field=线程名,value=1
redis.call('hset', KEYS[1], ARGV[2], 1);
// 设置 hash 的过期时间
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
// 锁名称存在,判断是否当前线程持有的锁
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
// 如果是,value+1,代表重入次数+1
redis.call('hincrby', KEYS[1], ARGV[2], 1);
// 重新获得锁,需要重新设置 Key 的过期时间
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
// 锁存在,但是不是当前线程持有,返回过期时间(毫秒)
return redis.call('pttl', KEYS[1]);

Функция unlockInnerAsync() в функции unlock() снимает блокировку, которая также реализуется сценарием Lua.

// KEYS[1] 锁的名称 updateAccount
// KEYS[2] 频道名称 redisson_lock__channel:{updateAccount}
// ARGV[1] 释放锁的消息 0
// ARGV[2] 锁释放时间 10000
// ARGV[3] 线程名称
// 锁不存在(过期或者已经释放了)
if (redis.call('exists', KEYS[1]) == 0) then
// 发布锁已经释放的消息
redis.call('publish', KEYS[2], ARGV[1]);
return 1;
end;
// 锁存在,但是不是当前线程加的锁
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then
return nil;
end;

// 锁存在,是当前线程加的锁
// 重入次数-1
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);
// -1 后大于 0,说明这个线程持有这把锁还有其他的任务需要执行
if (counter > 0) then
// 重新设置锁的过期时间
redis.call('pexpire', KEYS[1], ARGV[2]);
return 0;
else
// -1 之后等于 0,现在可以删除锁了
redis.call('del', KEYS[1]);
// 删除之后发布释放锁的消息
redis.call('publish', KEYS[2], ARGV[1]);
return 1;
end;

// 其他情况返回 nil
return nil;

Исходный код находится в com/xhj/distributedLock/LockTest.java.

Понаблюдав за его использованием, мы обнаружили, что он действительно так же гладок, как ReentrantLock в JDK. Вот еще несколько вопросов:

  • Что делать, если дело не доведено до конца и срок действия блокировки истек, это гарантирует сторожевой таймер.
  • В режиме кластера, если несколько мастеров заблокированы, что приводит к повторной блокировке, Redission автоматически выберет один и тот же мастер.
  • Что делать, если мастер Redis зависает до завершения дела?Не беда, у слейва Redis все еще есть эти данные.

RedLock

Китайский RedLock — это буквальный перевод, который называется Red Lock. Red lock — это не инструмент, а алгоритм распределенной блокировки, официально предложенный Redis. Мы знаем, что если будет принят режим развертывания на одной машине, проблема будет только в одном: до тех пор, пока не работает redis, блокировка не будет работать. Если принят режим ведущий-ведомый, при блокировке блокируется только один узел, даже если высокая доступность обеспечивается через дозорный, но если главный узел выходит из строя и происходит переключение ведущий-ведомый, в это время может возникнуть проблема потери блокировки. . Исходя из вышеизложенных соображений, на самом деле автор redis Antirez тоже рассматривал эту проблему, и он предложил алгоритм RedLock.

Я нарисовал диаграмму здесь. Пять экземпляров на диаграмме развернуты независимо. Отношения ведущий-ведомый отсутствуют. Это пять главных узлов.

Получите блокировку, выполнив следующие действия:

  • Получить текущую метку времени в миллисекундах
  • По очереди пытаемся создать блокировки на каждом главном узле, а время истечения устанавливается на короткое время, обычно десятки миллисекунд.
  • Попробуйте установить блокировку на большинстве узлов, скажем, для 5 узлов требуется 3 узла (n / 2 + 1)
  • Клиент вычисляет время установки блокировки, если время установки блокировки меньше тайм-аута, то установка прошла успешно.
  • Если установка блокировки не удалась, то удаляем блокировку по очереди
  • Пока кто-то другой установил распределенную блокировку, вы должны продолжать опрос, чтобы попытаться получить блокировку.

Однако такой алгоритм все еще довольно спорный, и проблем может быть еще много, и нет никакой гарантии, что процесс блокировки будет правильным. Мартин Клеппманн поставил под сомнение алгоритм, а затем антирез ответил на вопрос Мартина Клеппманна. Один — высококвалифицированный распределенный архитектор, а другой — отец Redis — знаменитой феерической драки про красные замки.

Наконец, на официальном сайте Redisson также показано, как использовать красный замок redlock.Это делается с помощью нескольких строк кода, и это все еще очень гладко.Заинтересованные друзья могут посмотреть.

Зоопарк пишет

Прежде чем представить механизм, с помощью которого zookeeper реализует распределенные блокировки, давайте кратко представим, что такое zk: zk — это централизованная служба, обеспечивающая управление конфигурацией, распределенную совместную работу и присвоение имен. Его модель такова: он содержит ряд узлов, называемых znodes, точно так же, как файловая система, каждый znode представляет собой каталог, а затем znode имеют некоторые характеристики, мы можем разделить их на четыре категории:

  • Постоянный узел (отключенный узел zk все еще существует)
  • Узел последовательности сохраняемости (если это первый созданный дочерний узел, сгенерированный дочерний узел — /lock/node-0000000000, следующий узел — /lock/node-0000000001 и т. д.)
  • Временные узлы (узлы удаляются после отключения клиента)
  • Узел временной последовательности

Распределенная блокировка zookeeper просто применяет узел временной последовательности Давайте посмотрим, как это реализовано схематически.

получить замок

Сначала создайте постоянный узел ParentLock в Zookeeper. Когда первый клиент хочет получить блокировку, ему необходимо создать временный узел последовательности Lock1 под узлом ParentLock.После этого Client1 находит и сортирует все узлы временной последовательности под ParentLock и оценивает, является ли созданный им узел Lock1 узлом с наивысшим порядком. Если это первый узел, блокировка успешно получена.В это время, если другой клиент Client2 приходит для получения блокировки, другой временный узел последовательности Lock2 создается под ParentLock.Client2 находит и сортирует все узлы временной последовательности под ParentLock и оценивает, является ли созданный им узел Lock2 узлом с наивысшим порядком.Оказывается, что узел Lock2 не самый маленький.

Таким образом, Client2 регистрирует Watcher с узлом Lock1, который находится только перед ним, чтобы отслеживать, существует ли узел Lock1. Это означает, что Client2 не смог захватить блокировку и перешел в состояние ожидания.В это время, если для получения блокировки приходит другой клиент Client3, в загрузке ParentLock создается временный узел последовательности Lock3.Client3 находит и сортирует все узлы временной последовательности в ParentLock и оценивает, является ли узел Lock3, созданный им самим, узлом с наивысшим порядком.Результат также обнаруживает, что узел Lock3 не является самым маленьким.

Поэтому Client3 регистрирует Watcher с узлом Lock2, который находится только перед ним, чтобы отслеживать, существует ли узел Lock2. Это означает, что Client3 также не смог захватить блокировку и перешел в состояние ожидания.Таким образом, Client1 получает блокировку, Client2 прослушивает Lock1, а Client3 прослушивает Lock2. Это просто формирует очередь ожидания, очень похожую на AQS (AbstractQueuedSynchronizer), на которую ReentrantLock опирается в Java.

разблокировать замок

Есть два случая снятия блокировки:

1. Задача выполнена, клиент выводит релиз

По завершении задачи Client1 отображает инструкцию по удалению узла Lock1.2. Во время выполнения задачи клиент вылетает

Во время выполнения задачи, если Duang рухнет с треском, Client1, получивший блокировку, отключится от сервера Zookeeper. В соответствии с характеристиками временного узла связанный с ним узел Lock1 будет автоматически удален.

Поскольку Client2 отслеживает существование Lock1, при удалении узла Lock1 Client2 будет немедленно уведомлен. В это время Client2 снова запросит все узлы под ParentLock, чтобы подтвердить, является ли узел Lock2, созданный им самим, самым маленьким узлом в настоящее время. Если он наименьший, Client2 получил блокировку логически.Точно так же, если Client2 также удалит узел Lock2, потому что задача завершена или узел выходит из строя, то Client3 будет уведомлен.Наконец, Client3 успешно получил блокировку.

Curator

В среду Apache Curator с открытым исходным кодом включена реализация распределенной блокировки Zookeeper.GitHub.com/Apache/Вдруг…

Он также очень прост в использовании, а именно:Мы его посмотрели и он был еще шелковистым.Я не буду анализировать исходный код.Если вам интересно, вы можете прочитать блог моего коллеги.Кураторский принцип реализации распределенного замка ZK.

Суммировать

Естественный дизайн Zookeeper — это распределенная координация, строгая согласованность и надежные замки. Если вы не можете получить блокировку, вам нужно только добавить прослушиватель, вам не нужно постоянно опрашивать, а потребление производительности невелико.недостаток:При высоких запросах и высоком параллелизме система лихорадочно блокирует и снимает блокировки, в итоге zk не выдерживает такого большого давления, и может возникнуть риск простоя.

Вот краткое упоминание о причине, по которой производительность zk lock ниже, чем у redis: роли в zk разделены на лидера и цветка, каждый запрос на запись может запрашивать только у лидера, а лидер будет транслировать запрос на запись всем цветкам, только если цветы будут успешными, они будут переданы лидеру, по сути, это эквивалентно процессу 2PC. При блокировке это запрос на запись.Когда запросов на запись много, zk будет иметь большое давление, что в конечном итоге приведет к медленному ответу сервера.

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

Наконец, независимо от redis или zookeeper, все они должны удовлетворятьОсобенности распределенных замков:

  • Реентерабельный (потоки, получившие блокировки, не должны снова получать блокировки во время выполнения)
  • Исключение или тайм-аут автоматически удаляются, чтобы избежать взаимоблокировки
  • Взаимное исключение, только один клиент может удерживать блокировку
  • Высокая производительность, высокая доступность, механизм отказоустойчивости в распределенной среде

Каждый из них имеет свои достоинства и используется в конкретных бизнес-сценариях.