Резюме реализации распределенной блокировки

Redis задняя часть база данных ZooKeeper

[TOC]

Резюме реализации распределенной блокировки

Много раз нам нужно гарантировать, что метод может быть вызван только одним и тем же потоком в одно и то же время.В автономной среде Java фактически предоставляет множество API, связанных с параллельной обработкой, но эти API бессильны в распределенных сценариях. То есть чистый Java Api не может обеспечить возможность распределенной блокировки.

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

  • Распределенный замок на основе базы данных
  • Распределенная блокировка на основе кеша (redis, memcached)
  • Распределенная блокировка на основе Zookeeper

Распределенная блокировка на основе базы данных

Простая реализация

Создайте таблицу напрямую и запишите ее锁定的方法名 时间Вот и все.
Когда требуется блокировка, часть данных вставляется, а когда блокировка снимается, данные удаляются.

CREATE TABLE `methodLock` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `method_name` varchar(64) NOT NULL DEFAULT '' COMMENT '锁定的方法名',
  `desc` varchar(1024) NOT NULL DEFAULT '备注信息',
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '保存数据时间,自动生成',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uidx_method_name` (`method_name `) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='锁定中的方法';

Когда мы хотим заблокировать метод, выполните следующий SQL:

insert into methodLock(method_name,desc) values (‘method_name’,‘desc’)

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

delete from methodLock where method_name ='method_name'
существующие проблемы
  1. Эта блокировка сильно зависит от доступности базы данных, база данных является единственной точкой, как только база данных зависнет, система сделает службу непригодной для использования.
  2. Эта блокировка не имеет срока действия, после сбоя операции разблокировки это приведет к тому, что блокировка будет записана в базу данных, другие потоки не смогут получить повторную блокировку.
  3. Эта блокировка может быть только неблокирующей, потому что операция вставки данных сразу сообщит об ошибке после сбоя вставки. Потоки, которые не получили блокировку, не будут попадать в очередь.Чтобы снова получить блокировку, операция получения блокировки запускается снова.
  4. Эта блокировка не допускает повторного входа, и тот же поток не может снова получить блокировку, не сняв блокировку. Потому что данные уже существуют в data.
Решение
  1. Для одноточечных проблем вы можете использовать несколько экземпляров базы данных и одновременно подключать таблицы N. Если N/2+1 выполнены успешно, задача будет успешно заблокирована.
  2. Напишите временную задачу, чтобы время от времени очищать просроченные данные.
  3. Напишите цикл while, который повторяет попытку вставки, пока не добьется успеха.
  4. Добавьте поле в таблицу базы данных, запишите информацию о хосте и информацию о потоке для текущего получателя блокировки, затем запросите базу данных, когда блокировка будет получена в следующий раз, если информацию о хосте и информацию о потоке текущей машины можно найти в базу данных, назначьте ему блокировку.
Суммировать

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

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

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

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

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

Существует много зрелых распределенных продуктов, включая Redis, memcache, Tair и т. д.

одноточечная реализация
шаг
  1. Чтобы использовать блокировку, используйте setnx для блокировки, установите значение в текущую метку времени, а затем используйте expire, чтобы установить значение истечения срока действия.
  2. Если блокировка получена, выполняется блок кода синхронизации.Если блокировка не получена, вы можете выбрать прокручивание, спящий режим или создание очереди ожидания для пробуждения процесса блокировки (аналогично очереди синхронизации в Synchronize) в соответствии с бизнес-сценарий Используйте ttl, чтобы проверить, есть ли какое-либо значение Expiration, если его нет, используйте expire, чтобы установить его.
  3. После завершения выполнения сначала оцените, является ли это вашей собственной блокировкой по значению value.Если да, то удалите ее.Если нет, то значит ваша блокировка просрочена и вам не нужно ее удалять. (В настоящее время существует проблема, заключающаяся в том, что несколько процессов одновременно блокируются из-за истечения срока действия)
существующие проблемы
  1. Единственный пункт вопроса. Если автономный редис зависнет, программа выдаст ошибку
  2. Если для передачи используется подчиненный узел, репликация не является синхронной репликацией, и возможны ситуации, когда несколько программ получают блокировки.
code
    public Object around(ProceedingJoinPoint joinPoint) {
        try {
            MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
            Method method = methodSignature.getMethod();

            DLock dLock = method.getAnnotation(DLock.class);
            if (dLock != null) {
                String lockedPrefix = buildLockedPrefix(dLock, method, joinPoint.getArgs());
                long timeOut = dLock.timeOut();
                int expireTime = dLock.expireTime();
                long value = System.currentTimeMillis();
                if (lock(lockedPrefix, timeOut, expireTime, value)) {
                    try {
                        return joinPoint.proceed();
                    } catch (Throwable throwable) {
                        throwable.printStackTrace();
                    } finally {
                        unlock(lockedPrefix, value);
                    }
                } else {
                    recheck(lockedPrefix, expireTime);
                }

            }
        } catch (Exception e) {
            logger.error("DLockAspect around error", e);
        }
        return null;
    }

    /**
     * 检查是否设置过超时
     *
     * @param lockedPrefix
     * @param expireTime
     */
    public void recheck(String lockedPrefix, int expireTime) {
        try {
            Result<Long> ttl = cacheFactory.getFactory().ttl(getLockedPrefix(lockedPrefix));
            if (ttl.isSuccess() && ttl.getValue() == -1) {
                Result<String> get = cacheFactory.getFactory().get(getLockedPrefix(lockedPrefix));
                //没有超时设置则设置超时
                if (get.isSuccess() && !StringUtils.isEmpty(get.getValue())) {
                    long oldTime = Long.parseLong(get.getValue());
                    long newTime = expireTime * 1000 - (System.currentTimeMillis() - oldTime);
                    if (newTime < 0) {
                        //已过超时时间  设默认最小超时时间
                        cacheFactory.getFactory().expire(getLockedPrefix(lockedPrefix), MIX_EXPIRE_TIME);
                    } else {
                        //未超过  设置为剩余超时时间
                        cacheFactory.getFactory().expire(getLockedPrefix(lockedPrefix), (int) newTime);
                    }
                    logger.info(lockedPrefix + "recheck:" + newTime);
                }
            }
            logger.info(String.format("执行失败lockedPrefix:%s count:%d", lockedPrefix, count++));
        } catch (Exception e) {
            logger.error("DLockAspect recheck error", e);
        }

    }
        public boolean lock(String lockedPrefix, long timeOut, int expireTime, long value) {
        long millisTime = System.currentTimeMillis();
        try {
            //在timeOut的时间范围内不断轮询锁
            while (System.currentTimeMillis() - millisTime < timeOut * 1000) {
                //锁不存在的话,设置锁并设置锁过期时间,即加锁
                Result<Long> result = cacheFactory.getFactory().setnx(getLockedPrefix(lockedPrefix), String.valueOf(value));
                if (result.isSuccess() && result.getValue() == 1) {

                    Result<Long> result1 = cacheFactory.getFactory().expire(getLockedPrefix(lockedPrefix), expireTime);
                    logger.info(lockedPrefix + "locked and expire " + result1.getValue());
                    return true;
                }
                //短暂休眠,避免可能的活锁
                Thread.sleep(100, RANDOM.nextInt(50000));
            }
        } catch (Exception e) {
            logger.error("lock error " + getLockedPrefix(lockedPrefix), e);
        }

        return false;
    }

    public void unlock(String lockedPrefix, long value) {
        try {
            Result<String> result = cacheFactory.getFactory().get(getLockedPrefix(lockedPrefix));
            String kvValue = result.getValue();
            if (!StringUtils.isEmpty(kvValue) && kvValue.equals(String.valueOf(value))) {
                cacheFactory.getFactory().del(getLockedPrefix(lockedPrefix));

            }
            logger.info(lockedPrefix + "unlock:" + kvValue + "----" + value);
        } catch (Exception e) {
            logger.error("unlock error" + getLockedPrefix(lockedPrefix), e);
        }
    }
RedLock

Redlock — это распределенная блокировка Redis в кластерном режиме, предоставленная antirez, автором Redis, Она основана на N полностью независимых узлах Redis (обычно N может быть установлено равным 5).

шаг
  1. Получить текущее время в миллисекундах.
  2. Чтобы последовательно выполнить работу приобретенного блокировки узлам N Redis. Эта операция по приобретению такая же, как процесс получения блокировки на основе одиночного узла Redis, включая случайную строку my_random_value, также содержит время истечения (например, px 30000, время действия блокировки). Чтобы обеспечить, чтобы алгоритм мог продолжать работать, когда узел Redis недоступен, этот замок приобретения имеет тайм-аут, что намного меньше, чем допустимое время блокировки (десятки миллисекунд). Если клиенту не удается получить блокировку от узла Redis, он должен немедленно попробовать следующий узел Redis. Сбой здесь должен включать в себя любой тип сбоя, например, узел Redis недоступен или блокировка узла Redis уже удерживается другими клиентами (Примечание. В первоначальном Redlock упоминался только случай, когда узел Redis был недоступен, но другие также должны быть включены неудачи).
  3. Подсчитайте, сколько времени в общей сложности занимает весь процесс получения блокировки.Метод расчета заключается в вычитании времени, записанного на шаге 1, из текущего времени. Если клиент успешно получает блокировку с большинства узлов Redis (>= N/2+1), а общее время, затраченное на получение блокировки, не превышает времени действия блокировки, то клиент считает окончательное получение блокировки Блокировка прошла успешно; в противном случае , считается, что окончательная блокировка не удалась.
  4. Если блокировка, наконец, получена успешно, то время действия блокировки должно быть пересчитано, что равно времени действия исходной блокировки за вычетом времени, затраченного на получение блокировки, рассчитанного на шаге 3.
  5. Если окончательное получение блокировки не удается (возможно, из-за того, что количество узлов Redis, которые получают блокировку, меньше N/2+1, или весь процесс получения блокировки занимает больше времени, чем начальное время действия блокировки), то клиент должен немедленно отправить все. Узел Redis инициирует операцию снятия блокировки.
оптимизация

Клиент 1 успешно заблокировал A, B, C и успешно получил блокировку (но D и E не были заблокированы); Узел C вышел из строя и перезапустился, но блокировка, добавленная Клиентом 1 на C, не была постоянной и потеряна; После перезапуска узла C , клиент 2 блокирует C, D и E, и блокировки успешно получены. Клиент 1 и Клиент 2 получают блокировки (для одного и того же ресурса) одновременно.

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

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

Для получения дополнительной информации о RedLock см.:

Действительно ли распределенная блокировка на основе Redis безопасна? (начальство)

Действительно ли распределенная блокировка на основе Redis безопасна? (Вниз)

Лучшая реализация:

Redission

Замок зоопарка

шаг
  1. Когда каждый клиент блокирует метод, в каталоге указанного узла создается уникальный мгновенный упорядоченный узел, соответствующий методу на zookeeper.
  2. Способ определить, следует ли приобретать блокировку, очень прост, вам нужно только определить блокировку с наименьшим порядковым номером в заказанном узле.
  3. При снятии блокировки просто удалите переходный узел. В то же время это может избежать проблемы взаимоблокировки, когда блокировка не может быть снята из-за простоя службы.
преимущество
  1. Нет проблемы с одной точкой. ZK развертывается в кластере, и пока более половины машин в кластере выживают, он может предоставлять внешние услуги.

  2. Удерживайте замок в течение любого промежутка времени, и замок может быть автоматически снят. Использование Zookeeper может эффективно решить проблему, что блокировка не может быть снята, потому что при создании блокировки клиент создаст временную ноду в ZK.Как только клиент получает блокировку и внезапно зависает (разрыв сеансового соединения), то этот временный узел Узел автоматически удаляется. Затем другие клиенты могут снова получить блокировку.Это позволяет избежать дилеммы о том, как долго должна устанавливаться блокировка на основе Redis в качестве срока действия блокировки.На самом деле блокировки на основе ZooKeeper полагаются на Session (пульс) для поддержания состояния удержания блокировки, в то время как Redis не поддерживает Session.

  3. Блокируемый. С помощью Zookeeper можно добиться блокировки блокировки.Клиенты могут создавать последовательные узлы в ZK и привязывать прослушиватели к узлам.Как только узлы изменятся, Zookeeper уведомит клиента, и клиент сможет проверить, является ли созданный им узел текущим.Узел с наименьшим серийный номер среди всех узлов, если он есть, то он получит блокировку и выполнит бизнес-логику.

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

проблема
  1. Эта практика может привести к羊群效应, тем самым снижая эффективность блокировки.
  2. Производительность не так хороша, как кэширование. Потому что каждый раз в процессе создания и снятия блокировки мгновенные узлы должны динамически создаваться и уничтожаться для реализации функции блокировки. Создание и удаление нод в ZK возможно только через сервер-лидер, и тогда данные не могут быть разделены со всеми машинами-последователями.

Лучшая реализация:

Curator

public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
    try {
        return interProcessMutex.acquire(timeout, unit);
    } catch (Exception e) {
        e.printStackTrace();
    }
    return true;
}
public boolean unlock() {
    try {
        interProcessMutex.release();
    } catch (Throwable e) {
        log.error(e.getMessage(), e);
    } finally {
        executorService.schedule(new Cleaner(client, path), delayTimeForClean, TimeUnit.MILLISECONDS);
    }
    return true;
}

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

Суммировать

Преимущества использования Zookeeper для реализации распределенных блокировок:Эффективно решать одноточечные проблемы, проблемы без повторного входа, неблокирующие проблемы и проблемы, которые не могут быть сняты. Его проще реализовать.

Недостатки использования Zookeeper для реализации распределенных блокировок:Лучше использовать распределенный кеш, чтобы добиться блокировки производительности. Нам нужно понять принципы ЗК.

Сравнение трех схем

С точки зрения простоты понимания (от низкого к высокому):База данных > Кэш > Zookeeper

С точки зрения реализации сложность (от низкой до высокой):Zookeeper> = Cache> База данных

С точки зрения производительности (от высокого к низкому):Кэш> Zookeeper> = База данных

С точки зрения надежности (от высокого к низкому):Zookeeper > Кэш > База данных