[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'
существующие проблемы
- Эта блокировка сильно зависит от доступности базы данных, база данных является единственной точкой, как только база данных зависнет, система сделает службу непригодной для использования.
- Эта блокировка не имеет срока действия, после сбоя операции разблокировки это приведет к тому, что блокировка будет записана в базу данных, другие потоки не смогут получить повторную блокировку.
- Эта блокировка может быть только неблокирующей, потому что операция вставки данных сразу сообщит об ошибке после сбоя вставки. Потоки, которые не получили блокировку, не будут попадать в очередь.Чтобы снова получить блокировку, операция получения блокировки запускается снова.
- Эта блокировка не допускает повторного входа, и тот же поток не может снова получить блокировку, не сняв блокировку. Потому что данные уже существуют в data.
Решение
- Для одноточечных проблем вы можете использовать несколько экземпляров базы данных и одновременно подключать таблицы N. Если N/2+1 выполнены успешно, задача будет успешно заблокирована.
- Напишите временную задачу, чтобы время от времени очищать просроченные данные.
- Напишите цикл while, который повторяет попытку вставки, пока не добьется успеха.
- Добавьте поле в таблицу базы данных, запишите информацию о хосте и информацию о потоке для текущего получателя блокировки, затем запросите базу данных, когда блокировка будет получена в следующий раз, если информацию о хосте и информацию о потоке текущей машины можно найти в базу данных, назначьте ему блокировку.
Суммировать
Преимущества реализации распределенных блокировок в базах данных:Это легко понять непосредственно с помощью базы данных.
Недостатки реализации распределенных блокировок в базах данных:Будут всевозможные проблемы, которые будут усложнять всю схему в процессе решения задач.
Работа с базой данных требует определенных накладных расходов, и необходимо учитывать проблемы с производительностью.
Распределенная блокировка на основе кеша
По сравнению с использованием базы данных для реализации распределенных блокировок производительность распределенных блокировок, реализованных на основе кэшей, будет лучше.
Существует много зрелых распределенных продуктов, включая Redis, memcache, Tair и т. д.
одноточечная реализация
шаг
- Чтобы использовать блокировку, используйте setnx для блокировки, установите значение в текущую метку времени, а затем используйте expire, чтобы установить значение истечения срока действия.
- Если блокировка получена, выполняется блок кода синхронизации.Если блокировка не получена, вы можете выбрать прокручивание, спящий режим или создание очереди ожидания для пробуждения процесса блокировки (аналогично очереди синхронизации в Synchronize) в соответствии с бизнес-сценарий Используйте ttl, чтобы проверить, есть ли какое-либо значение Expiration, если его нет, используйте expire, чтобы установить его.
- После завершения выполнения сначала оцените, является ли это вашей собственной блокировкой по значению value.Если да, то удалите ее.Если нет, то значит ваша блокировка просрочена и вам не нужно ее удалять. (В настоящее время существует проблема, заключающаяся в том, что несколько процессов одновременно блокируются из-за истечения срока действия)
существующие проблемы
- Единственный пункт вопроса. Если автономный редис зависнет, программа выдаст ошибку
- Если для передачи используется подчиненный узел, репликация не является синхронной репликацией, и возможны ситуации, когда несколько программ получают блокировки.
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).
шаг
- Получить текущее время в миллисекундах.
- Чтобы последовательно выполнить работу приобретенного блокировки узлам N Redis. Эта операция по приобретению такая же, как процесс получения блокировки на основе одиночного узла Redis, включая случайную строку my_random_value, также содержит время истечения (например, px 30000, время действия блокировки). Чтобы обеспечить, чтобы алгоритм мог продолжать работать, когда узел Redis недоступен, этот замок приобретения имеет тайм-аут, что намного меньше, чем допустимое время блокировки (десятки миллисекунд). Если клиенту не удается получить блокировку от узла Redis, он должен немедленно попробовать следующий узел Redis. Сбой здесь должен включать в себя любой тип сбоя, например, узел Redis недоступен или блокировка узла Redis уже удерживается другими клиентами (Примечание. В первоначальном Redlock упоминался только случай, когда узел Redis был недоступен, но другие также должны быть включены неудачи).
- Подсчитайте, сколько времени в общей сложности занимает весь процесс получения блокировки.Метод расчета заключается в вычитании времени, записанного на шаге 1, из текущего времени. Если клиент успешно получает блокировку с большинства узлов Redis (>= N/2+1), а общее время, затраченное на получение блокировки, не превышает времени действия блокировки, то клиент считает окончательное получение блокировки Блокировка прошла успешно; в противном случае , считается, что окончательная блокировка не удалась.
- Если блокировка, наконец, получена успешно, то время действия блокировки должно быть пересчитано, что равно времени действия исходной блокировки за вычетом времени, затраченного на получение блокировки, рассчитанного на шаге 3.
- Если окончательное получение блокировки не удается (возможно, из-за того, что количество узлов Redis, которые получают блокировку, меньше N/2+1, или весь процесс получения блокировки занимает больше времени, чем начальное время действия блокировки), то клиент должен немедленно отправить все. Узел Redis инициирует операцию снятия блокировки.
оптимизация
Клиент 1 успешно заблокировал A, B, C и успешно получил блокировку (но D и E не были заблокированы); Узел C вышел из строя и перезапустился, но блокировка, добавленная Клиентом 1 на C, не была постоянной и потеряна; После перезапуска узла C , клиент 2 блокирует C, D и E, и блокировки успешно получены. Клиент 1 и Клиент 2 получают блокировки (для одного и того же ресурса) одновременно.
Эта проблема может задержать время восстановления узла, и продолжительность времени должна быть больше или равна сроку действия блокировки.
существующие проблемы
- Скачок поколения часов. Когда это происходит, прямой результат тайм-аута всех блокировок, успех нового потока может получить блокировку, что приводит к многопоточной одновременной обработке.
Для получения дополнительной информации о RedLock см.:
Действительно ли распределенная блокировка на основе Redis безопасна? (начальство)
Действительно ли распределенная блокировка на основе Redis безопасна? (Вниз)
Лучшая реализация:
Замок зоопарка
шаг
- Когда каждый клиент блокирует метод, в каталоге указанного узла создается уникальный мгновенный упорядоченный узел, соответствующий методу на zookeeper.
- Способ определить, следует ли приобретать блокировку, очень прост, вам нужно только определить блокировку с наименьшим порядковым номером в заказанном узле.
- При снятии блокировки просто удалите переходный узел. В то же время это может избежать проблемы взаимоблокировки, когда блокировка не может быть снята из-за простоя службы.
преимущество
-
Нет проблемы с одной точкой. ZK развертывается в кластере, и пока более половины машин в кластере выживают, он может предоставлять внешние услуги.
-
Удерживайте замок в течение любого промежутка времени, и замок может быть автоматически снят. Использование Zookeeper может эффективно решить проблему, что блокировка не может быть снята, потому что при создании блокировки клиент создаст временную ноду в ZK.Как только клиент получает блокировку и внезапно зависает (разрыв сеансового соединения), то этот временный узел Узел автоматически удаляется. Затем другие клиенты могут снова получить блокировку.Это позволяет избежать дилеммы о том, как долго должна устанавливаться блокировка на основе Redis в качестве срока действия блокировки.На самом деле блокировки на основе ZooKeeper полагаются на Session (пульс) для поддержания состояния удержания блокировки, в то время как Redis не поддерживает Session.
-
Блокируемый. С помощью Zookeeper можно добиться блокировки блокировки.Клиенты могут создавать последовательные узлы в ZK и привязывать прослушиватели к узлам.Как только узлы изменятся, Zookeeper уведомит клиента, и клиент сможет проверить, является ли созданный им узел текущим.Узел с наименьшим серийный номер среди всех узлов, если он есть, то он получит блокировку и выполнит бизнес-логику.
-
реентерабельный. Когда клиент создает узел, он напрямую записывает информацию о хосте и потоке текущего клиента в узел.В следующий раз, когда он захочет получить блокировку, ее можно будет сравнить с данными в текущем наименьшем узле. Если информация совпадает с вашей собственной, то вы можете напрямую получить блокировку, если нет, создать временный узел последовательности для участия в очереди.
проблема
- Эта практика может привести к
羊群效应
, тем самым снижая эффективность блокировки. - Производительность не так хороша, как кэширование. Потому что каждый раз в процессе создания и снятия блокировки мгновенные узлы должны динамически создаваться и уничтожаться для реализации функции блокировки. Создание и удаление нод в ZK возможно только через сервер-лидер, и тогда данные не могут быть разделены со всеми машинами-последователями.
Лучшая реализация:
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 > Кэш > База данных