Redis или Zookeeper для распределенных замков?

Java

Зачем использовать распределенные блокировки?

Прежде чем обсуждать этот вопрос, давайте рассмотрим бизнес-сценарий:

Система А — это система электронной коммерции. В настоящее время она развернута на компьютере. В системе есть интерфейс для размещения заказов пользователями, но перед размещением заказа пользователи должны проверить запасы, чтобы убедиться, что запасов достаточно, прежде чем разместить заказ. заказ для пользователя.

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

Архитектура системы следующая:

Но тогда это будетсоздать проблему: Если в какой-то момент инвентарь определенного товара в redis равен 1, в это время одновременно приходят два запроса, один из запросов выполняется до третьего шага на рисунке выше, а инвентарь обновляется база данных равна 0, но четвертый шаг не выполнен.

И выполняется другой запрос к шагу 2, и обнаруживается, что инвентарь по-прежнему равен 1, и он продолжает выполнять шаг 3.

В итоге было продано 2 шт., а по факту в наличии была только 1 шт.

Очевидно неправильно! Это классическая проблема перепроданности.

На этом этапе мы можем легко придумать решение: заблокировать шаги 2, 3 и 4 блокировкой и позволить им завершиться до того, как другой поток сможет войти и выполнить шаг 2.

Согласно приведенному выше рисунку, при выполнении шага 2 используйте синхронизированный или ReentrantLock, предоставленный Java, для блокировки, а затем снимите блокировку после выполнения шага 4.

Таким образом, три шага 2, 3 и 4 «заблокированы», и несколько потоков могут выполняться только последовательно.

Но хорошие времена длились недолго, параллелизм всей системы взлетел до небес, и одна машина с этим не справилась. Теперь добавим машину, как показано ниже:

После добавления машин система становится такой, как показано на картинке выше, боже мой!

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

Зачем? Поскольку две системы A на рисунке работают на двух разных JVM, они оснащены потоками, принадлежащими их JVM, что недопустимо для других потоков JVM.

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

Это связано с тем, что блокировки, добавленные двумя машинами, не являются одной и той же блокировкой (две блокировки находятся в разных JVM).

Итак, пока мы гарантируем, что блокировки, добавленные двумя машинами, являются одной и той же блокировкой, проблема не решена?

На данный момент пришло время для дебюта распределенных блокировок Идея распределенных блокировок заключается в следующем:

обеспечить общесистемныйглобальный, уникальный«Вещь», которая получает блокировку, а затем каждая система просит эту «вещь» получить блокировку, когда она должна быть заблокирована, так что то, что получают разные системы, можно рассматривать как одну и ту же блокировку.

Что касается этой «вещи», то это может быть Redis, Zookeeper или база данных.

Текстовое описание не очень интуитивное, давайте взглянем на следующую картинку:

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

Итак, как реализовать распределенную блокировку? Тогда смотри вниз!

Распределенная блокировка на основе Redis

Приведенный выше анализ объясняет, почему используются распределенные блокировки.Здесь мы рассмотрим, как поступать с распределенными блокировками, когда они приземляются. Расширение: как Redisson реализует распределенные блокировки?

Наиболее распространенным решением является использование Redis в качестве распределенной блокировки.

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

Конкретный код таков:

// 获取锁// NX是指如果key不存在就成功,key存在返回false,PX可以指定过期时间SET anyLock unique_value NX PX 30000// 释放锁:通过执行一段lua脚本// 释放锁涉及到两条指令,这两条指令不是原子性的// 需要用到redis的lua脚本支持特性,redis执行lua脚本是原子性的if redis.call("get",KEYS[1]) == ARGV[1] thenreturn redis.call("del",KEYS[1])elsereturn 0end

В этом подходе есть несколько ключевых моментов:

  • Обязательно используйте команду SET key value NX PX миллисекунд.

    Если нет, сначала установите значение, а затем установите время истечения.Это не атомарная операция.Операция может завершиться сбоем до того, как будет установлено время истечения, что приведет к взаимоблокировке (ключ существует постоянно).

  • значение должно быть уникальным

    Это делается для удаления ключа, когда он разблокирован, необходимо убедиться, что значение соответствует заблокированному значению.

    Это делается для того, чтобы избежать ситуации: предположим, что A получает блокировку, а время истечения составляет 30 с. Через 35 с блокировка была автоматически снята, и A собирается снять блокировку, но B может получить блокировку в это время. Клиент не может удалить блокировку B.

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

Есть 3 способа развернуть Redis:

  • Автономный режим

  • ведущий-ведомый + дозорный режим выбора

  • кластерный режим Redis

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

В режиме master-slave при блокировке блокируется только один узел.Даже если высокая доступность достигается с помощью Sentinel, в случае сбоя главного узла и переключения master-slave в это время может возникнуть проблема потери блокировки.

Исходя из вышеизложенных соображений, собственно, автор redis тоже рассматривал эту проблему.Он предложил алгоритм RedLock.Смысл этого алгоритма, вероятно, заключается в следующем:

Предполагая, что режим развертывания Redis — кластер Redis, всего имеется 5 главных узлов, и блокировка получается с помощью следующих шагов:

  • Получить текущую метку времени в миллисекундах

  • По очереди пытаемся создать блокировку на каждом главном узле, а время истечения устанавливается на короткое время, обычно десятки миллисекунд.

  • Попробуйте установить блокировку на большинстве узлов, скажем, для 5 узлов требуется 3 узла (n / 2 + 1)

  • Клиент вычисляет время для установки блокировки.Если время для установки блокировки меньше, чем период тайм-аута, установка прошла успешно.

  • Если установка блокировки не удалась, то удаляем блокировку по очереди

  • Пока кто-то другой установил распределенную блокировку, вы должны продолжать опрос, чтобы попытаться получить блокировку.

Однако такой алгоритм все еще довольно спорный, и проблем может быть еще много, и нет никакой гарантии, что процесс блокировки будет правильным.

Другой способ: Редиссон

Кроме того, для реализации распределенной блокировки Redis, помимо реализации на основе собственного API клиента Redis, вы также можете использовать фреймворк с открытым исходным кодом: Redission

Redisson — это клиент Redis с открытым исходным кодом корпоративного уровня, который также обеспечивает поддержку распределенных блокировок. Я также настоятельно рекомендую всем использовать его, почему?

Напомним, что я сказал выше, если вы пишете свой собственный код для установки значения через redis, оно устанавливается следующей командой.

  • SET anyLock unique_value NX PX 30000

Установленный здесь тайм-аут равен 30 с. Если я не выполнял бизнес-логику более 30 с, срок действия ключа истечет, и другие потоки могут получить блокировку.

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

Посмотрим, как реализован редиссон? Сначала почувствуйте крутость использования redission:

Config config = new Config();config.useClusterServers().addNodeAddress("redis://192.168.31.101:7001").addNodeAddress("redis://192.168.31.101:7002").addNodeAddress("redis://192.168.31.101:7003").addNodeAddress("redis://192.168.31.102:7001").addNodeAddress("redis://192.168.31.102:7002").addNodeAddress("redis://192.168.31.102:7003");RedissonClient redisson = Redisson.create(config);RLock lock = redisson.getLock("anyLock");lock.lock();lock.unlock();

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

  • Все инструкции redisson выполняются через lua-скрипты, а redis поддерживает атомарное выполнение lua-скриптов.

  • Redisson устанавливает срок действия ключа по умолчанию на 30 с. Что делать, если клиент удерживает блокировку более 30 с?

    В Редиссоне есть одинwatchdogКонцепция перевода - это сторожевой таймер, он поможет вам установить время ожидания ключа на 30 секунд каждые 10 секунд после получения блокировки.

    В этом случае, даже если блокировка удерживается все время, срок действия ключа не истечет, и блокировку получат другие потоки.

  • Логика «сторожевого таймера» Redisson гарантирует отсутствие взаимоблокировок.

    (Если машина не работает, сторожевой таймер также исчезнет. В это время срок действия ключа не будет продлен, и он автоматически истечет через 30 с, и другие потоки могут получить блокировку)

Вот немного кода его реализации:

/ 加锁逻辑private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, final long threadId) {    if (leaseTime != -1) {        return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);    }    // 调用一段lua脚本,设置一些key、过期时间    RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);    ttlRemainingFuture.addListener(new FutureListener<Long>() {        @Override        public void operationComplete(Future<Long> future) throws Exception {            if (!future.isSuccess()) {                return;            }            Long ttlRemaining = future.getNow();            // lock acquired            if (ttlRemaining == null) {                // 看门狗逻辑                scheduleExpirationRenewal(threadId);            }        }    });    return ttlRemainingFuture;}<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {    internalLockLeaseTime = unit.toMillis(leaseTime);    return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,              "if (redis.call('exists', KEYS[1]) == 0) then " +                  "redis.call('hset', KEYS[1], ARGV[2], 1); " +                  "redis.call('pexpire', KEYS[1], ARGV[1]); " +                  "return nil; " +              "end; " +              "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +                  "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +                  "redis.call('pexpire', KEYS[1], ARGV[1]); " +                  "return nil; " +              "end; " +              "return redis.call('pttl', KEYS[1]);",                Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));}// 看门狗最终会调用了这里private void scheduleExpirationRenewal(final long threadId) {    if (expirationRenewalMap.containsKey(getEntryName())) {        return;    }    // 这个任务会延迟10s执行    Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {        @Override        public void run(Timeout timeout) throws Exception {            // 这个操作会将key的过期时间重新设置为30s            RFuture<Boolean> future = renewExpirationAsync(threadId);            future.addListener(new FutureListener<Boolean>() {                @Override                public void operationComplete(Future<Boolean> future) throws Exception {                    expirationRenewalMap.remove(getEntryName());                    if (!future.isSuccess()) {                        log.error("Can't update lock " + getName() + " expiration", future.cause());                        return;                    }                    if (future.getNow()) {                        // reschedule itself                        // 通过递归调用本方法,无限循环延长过期时间                        scheduleExpirationRenewal(threadId);                    }                }            });        }    }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);    if (expirationRenewalMap.putIfAbsent(getEntryName(), new ExpirationEntry(threadId, task)) != null) {        task.cancel();    }}

Кроме того, redisson также обеспечивает поддержку алгоритма redlock, и его использование также очень простое:

RedissonClient redisson = Redisson.create(config);
RLock lock1 = redisson.getFairLock("lock1");
RLock lock2 = redisson.getFairLock("lock2");
RLock lock3 = redisson.getFairLock("lock3");
RedissonRedLock multiLock = new RedissonRedLock(lock1, lock2, lock3);multiLock.lock();multiLock.unlock();小结:本节分析了使用redis作为分布式锁的具体落地方案以及其一些局限性然后介绍了一个redis的客户端框架redisson,这也是我推荐大家使用的,比自己写代码实现会少care很多细节。

резюме:

В этом разделе анализируется конкретный план реализации использования Redis в качестве распределенной блокировки и некоторые из его ограничений, а затем представлена ​​клиентская среда Redis, Redisson, которую я рекомендую всем использовать.Это позаботится о многих деталях, чем самостоятельное написание кода.

Распределенная блокировка на основе zookeeper

Среди распространенных схем реализации распределенных блокировок, помимо использования Redis для реализации распределенных блокировок, Zookeeper также может использоваться для реализации распределенных блокировок.

Прежде чем представить механизм zookeeper (замененный на zk ниже) для реализации распределенных блокировок, давайте кратко представим, что такое zk:

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

Модель zk выглядит следующим образом: zk содержит ряд узлов, называемых znode, точно так же, как файловая система, каждый znode представляет собой каталог, а затем znode имеет некоторые характеристики:

  • Упорядоченный узел: если в настоящее время есть родительский узел как/lock, мы можем создать дочерние узлы под этим родительским узлом;

    Zookeeper предоставляет необязательную упорядоченную функцию.Например, мы можем создать дочерний узел «/lock/node-» и указать порядок, тогда zookeeper автоматически добавит целочисленный порядковый номер в соответствии с текущим количеством дочерних узлов при создании дочерних узлов.

    То есть, если это первый созданный дочерний узел, результирующий дочерний узел/lock/node-0000000000, следующий узел/lock/node-0000000001,И так далее.

  • Временный узел: клиент может установить временный узел, который будет автоматически удален zookeeper после завершения сеанса или истечения времени сеанса.

  • Мониторинг событий: при чтении данных мы можем одновременно установить мониторинг событий на узле.Когда данные или структура узла изменяются, zookeeper уведомит клиента. В настоящее время у зоопарка есть следующие четыре события:

  • Создание узла

  • Удаление узла

  • Модификация данных узла

  • изменение дочернего узла

Основываясь на некоторых характеристиках zk, приведенных выше, мы можем легко найти решение для использования zk для реализации распределенных блокировок:

  1. Используя временные узлы zk и упорядоченные узлы, каждый поток, получающий блокировку, создает временный упорядоченный узел в zk, например, в каталоге /lock/.

  2. После успешного создания узла получите все временные узлы в каталоге /lock, а затем определите, является ли узел, созданный текущим потоком, узлом с наименьшим порядковым номером среди всех узлов.

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

  4. Если узел, созданный текущим потоком, не является узлом с наименьшим порядковым номером узла, прослушиватель событий добавляется к узлу перед порядковым номером узла.

    Например, порядковый номер узла, полученный текущим потоком, равен/lock/003, то список всех узлов[/lock/001,/lock/002,/lock/003], тогда правильно/lock/002Этот узел добавляет прослушиватель событий.

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

Например/lock/001выпущенный,/lock/002Когда время отслеживается, набор узлов[/lock/002,/lock/003],но/lock/002Для узла с наименьшим порядковым номером устанавливается блокировка.

Весь процесс выглядит следующим образом:

Конкретная идея реализации такова, что касается написания кода, то это сложнее и здесь не будет.

Знакомство с куратором

Curator — это клиент zookeeper с открытым исходным кодом, который также обеспечивает реализацию распределенных блокировок.

Он также относительно прост в использовании:

InterProcessMutex interProcessMutex = new InterProcessMutex(client,"/anyLock");interProcessMutex.acquire();interProcessMutex.release();

Основной исходный код для реализации распределенных блокировок выглядит следующим образом:

private boolean internalLockLoop(long startMillis, Long millisToWait, String ourPath) throws Exception{    boolean  haveTheLock = false;    boolean  doDelete = false;    try {        if ( revocable.get() != null ) {            client.getData().usingWatcher(revocableWatcher).forPath(ourPath);        }        while ( (client.getState() == CuratorFrameworkState.STARTED) && !haveTheLock ) {            // 获取当前所有节点排序后的集合            List<String>        children = getSortedChildren();            // 获取当前节点的名称            String              sequenceNodeName = ourPath.substring(basePath.length() + 1); // +1 to include the slash            // 判断当前节点是否是最小的节点            PredicateResults    predicateResults = driver.getsTheLock(client, children, sequenceNodeName, maxLeases);            if ( predicateResults.getsTheLock() ) {                // 获取到锁                haveTheLock = true;            } else {                // 没获取到锁,对当前节点的上一个节点注册一个监听器                String  previousSequencePath = basePath + "/" + predicateResults.getPathToWatch();                synchronized(this){                    Stat stat = client.checkExists().usingWatcher(watcher).forPath(previousSequencePath);                    if ( stat != null ){                        if ( millisToWait != null ){                            millisToWait -= (System.currentTimeMillis() - startMillis);                            startMillis = System.currentTimeMillis();                            if ( millisToWait <= 0 ){                                doDelete = true;    // timed out - delete our node                                break;                            }                            wait(millisToWait);                        }else{                            wait();                        }                    }                }                // else it may have been deleted (i.e. lock released). Try to acquire again            }        }    }    catch ( Exception e ) {        doDelete = true;        throw e;    } finally{        if ( doDelete ){            deleteOurPath(ourPath);        }    }    return haveTheLock;}

Фактически, лежащий в основе кураторской реализации распределенных блокировок принцип аналогичен рассмотренному выше. Здесь мы используем картинку, чтобы подробно описать его принцип:

резюме:

В этом разделе представлена ​​схема zookeeperr для реализации распределенных блокировок и базовое использование клиента zk с открытым исходным кодом, а также кратко представлен принцип его реализации. Для получения соответствующей информации см.: Решение Live ZooKeeper для реализации распределенных блокировок с примерами!

Сравнение преимуществ и недостатков двух схем

После изучения двух реализаций распределенных блокировок в этом разделе необходимо обсудить преимущества и недостатки реализаций redis и zk.

Для распределенных замков redis он имеет следующие недостатки:

  • Он получает блокировку простым и грубым способом: если блокировку получить невозможно, он постоянно пытается получить блокировку напрямую, что снижает производительность.

  • Кроме того, дизайн и расположение Redis определяет, что его данные не являются строго согласованными, и в некоторых крайних случаях могут возникнуть проблемы. Модель замка недостаточно надежна

  • Даже если она реализована с использованием алгоритма redlock, в некоторых сложных сценариях нет гарантии, что она будет реализована на 100% без проблем Обсуждение redlock см. в разделе Как сделать распределенную блокировку

  • Распределенные блокировки Redis на самом деле должны пытаться получить блокировки сами по себе, что потребляет больше производительности.

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

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

Для распределенных замков zk:

  • Естественный дизайн Zookeeper — это распределенная координация и строгая согласованность. Модель замка надежна, проста в использовании и подходит для распределенных замков.

  • Если вы не можете получить блокировку, вам нужно только добавить прослушиватель, вам не нужно постоянно опрашивать, а потребление производительности невелико.

Однако у zk есть и свои недостатки: чем больше клиентов будут часто запрашивать блокировки и снимать блокировки, тем больше будет нагрузка на кластер zk.

резюме:

Подводя итог, у redis и zookeeper есть свои плюсы и минусы. Мы можем использовать эти вопросы в качестве справочных факторов при выборе технических решений.

предложение

Согласно предыдущему анализу, есть два распространенных решения для реализации распределенных блокировок: redis и zookeeper, каждое из которых имеет свои достоинства. Как его следует выбирать?

Лично,Я предпочитаю замок, реализованный ZK:

Поскольку в Redis могут быть скрытые опасности, данные могут быть неверными. Однако, как выбрать сцену, чтобы увидеть конкретные в компании.

Если в компании есть условия zk-кластера, предпочтительнее внедрение zk, но если в компании есть только кластер redis, условия для сборки zk-кластера отсутствуют.

Тогда это можно реализовать с помощью redis.Кроме того, может быть так, что системный разработчик считает, что в системе уже есть redis, но не хочет повторно вводить какие-то внешние зависимости, поэтому можно использовать redis.

Это должны учитывать разработчики систем в зависимости от архитектуры.

Следуйте за нами для получения дополнительной информации! ! !