Подготовка окружающей среды
Я предпочитаю делать полный набор примеров приложений распределенных блокировок Redis.Я подготовил различные среды Redis, развернул две службы с помощью SpringBoot, использовал tengine для балансировки нагрузки этих двух служб и использовал Jmeter для стресс-тестирования. , у него есть все внутренние органы.
Я подготовил статью о построении различных режимов окружения Redis:
Критика и исправления приветствуются.
В этой статье отрабатываются распределенные блокировки Redis из среды Redis single node, master-slave, sentinel и кластера.На самом деле главное это конфигурация.Если конфигурация правильная,то можно вызвать интерфейс.
Я подготовил различные среды для Redis, и наша реализация распределенного кода блокировки основана на этой серии сред.
один узел
имя хоста | Роль | айпи адрес | порт |
---|---|---|---|
redis-standalone | 192.168.2.11 | 6379 |
Ведущий-ведомый (1 ведущий и 3 ведомых)
имя хоста | Роль | айпи адрес | порт |
---|---|---|---|
Redis-Master-01 | Master | 192.168.2.20 | 9736 |
Redis-Slave-02 | Slave | 192.168.2.21 | 9736 |
Redis-Slave-03 | Slave | 192.168.2.22 | 9736 |
Redis-Slave-04 | Slave | 192.168.2.23 | 9736 |
Sentinel (1 главный, 3 подчиненных, 3 часовых)
имя хоста | Роль | айпи адрес | порт |
---|---|---|---|
Redis-Master-01 | Master | 192.168.2.20 | 9736 |
Redis-Slave-02 | Slave | 192.168.2.21 | 9736 |
Redis-Slave-03 | Slave | 192.168.2.22 | 9736 |
Redis-Slave-04 | Slave | 192.168.2.23 | 9736 |
Redis-Sentinel-01 | Sentinel | 192.168.2.30 | 29736 |
Redis-Sentinel-02 | Sentinel | 192.168.2.31 | 29736 |
Redis-Sentinel-03 | Sentinel | 192.168.2.32 | 29736 |
Кластер (3 ведущих и 3 подчиненных)
имя хоста | Роль | айпи адрес | порт |
---|---|---|---|
Redis-Cluster-01 | Master | 192.168.2.50 | 6379 |
Redis-Cluster-01 | Slave | 192.168.2.50 | 6380 |
Redis-Cluster-02 | Master | 192.168.2.51 | 6379 |
Redis-Cluster-02 | Slave | 192.168.2.51 | 6380 |
Redis-Cluster-03 | Master | 192.168.2.52 | 6379 |
Redis-Cluster-03 | Slave | 192.168.2.52 | 6380 |
Пример приложения распределенной блокировки
Я боролся с этим раньшеetcdа такжеzookeeperРеализация распределенной блокировки на примереспайк сцена, вычитая инвентарь, что также является классическим бизнес-сценарием использования распределенных блокировок.
Есть ли лучшая реализация распределенной блокировки, чем Redis? Да, и тд!
Реализация распределенных блокировок с помощью ZooKeeper
На этот раз по-другому, мы выполняем распределенные операции над объемом чтения статьи и используем распределенные блокировки Redis дляКоличество прочитанных статейЭтот общий ресурс находится под контролем.
# 存储阅读量
set pview 0
Используйте tengine (nginx) для балансировки нагрузки
информация о хозяине тенгине:
имя хоста | Роль | айпи адрес | порт |
---|---|---|---|
nginx-node | балансировки нагрузки | 192.168.2.10 | 80 |
В процессе выполнения стресс-теста позже будет использоваться только один адрес для балансировки нагрузки двух сервисов (8080/8090).Простая конфигурация nginx выглядит следующим образом:
...
upstream distributed-lock {
server 192.168.2.1:8080 weight=1;
server 192.168.2.1:8090 weight=1;
}
server {
listen 80;
server_name localhost;
location / {
root html;
index index.html index.htm;
proxy_pass http://distributed-lock;
}
...
}
...
Конфигурация стресс-теста JMeter
Имитация 666 запросов одновременно:
Колеса: Редиссон
Redisson
Существует неплохая поддержка реализации распределенных блокировок Redis и механизм ее реализации:
(1) Механизм блокировки: выберите клиента в соответствии с хэш-узлом для выполнения сценария lua.
(2) Блокировка механизма взаимного исключения: если другой клиент выполняет тот же lua-скрипт, он подскажет, что есть блокировка, а затем войдет в цикл и попытается заблокировать
(3) Реентерабельный механизм
(4) Механизм автоматического выдвижения сторожевой собачки
(5) Разблокируйте механизм блокировки
Код
не заблокирован
@RequestMapping("/v1/pview")
public String incrPviewWithoutLock() {
//阅读量增加1
long pview = redissonClient.getAtomicLong("pview").incrementAndGet();
LOGGER.info("{}线程执行阅读量加1,当前阅读量:{}", Thread.currentThread().getName(), pview);
return port + " increase pview end!";
}
При этом одновременных запросов 666. Посмотрим на результаты:
666 запросов, итоговый результат 34!
Добавить синхронизированную блокировку синхронизации
Только что из результатов видно, что в процессах JVM 8080 и 8090 есть дубликаты, поэтому давайте улучшим его и добавим одинsynchronized
Блокировку синхронизации, а потом смотреть на реализацию.
@RequestMapping("/v2/pview")
public String incrPviewWithSync() {
synchronized (this) {
//阅读量增加1
int oldPview = Integer.valueOf((String) redissonClient.getBucket("pview", new StringCodec()).get());
int newPview = oldPview + 1;
redissonClient.getBucket("pview", new StringCodec()).set(String.valueOf(newPview));
LOGGER.info("{}线程执行阅读量加1,当前阅读量:{}", Thread.currentThread().getName(), newPview);
}
return port + " increase pview end!";
}
Результат не 666, как ожидалось, а 391:
В это время видно, что хотяНет дублирования в рамках соответствующих сервисов двух портов.Да, но процессы сервисов 8080 и 8090 многократно добавляют +1 к одному и тому же значению pview.
То есть,Synchronized может решить проблему параллелизма только внутри процесса,Он не может решить проблему совместного использования ресурсов распределенными системами..
Дебют главного героя — распределенная блокировка
Чтобы решить проблему использования общих ресурсов в распределенных системах, используйтеРаспределенная блокировка.
Полный код:GitHub.com/Ответственность маленького белого дракона/День третий…
Построить RedissonClient:
public PviewController(RedisConfiguration redisConfiguration) {
RedissonManager redissonManager;
switch (redisConfiguration.deployType) {
case "single":
redissonManager = new SingleRedissonManager();
break;
case "master-slave":
redissonManager = new MasterSlaveRedissonManager();
break;
case "sentinel":
redissonManager = new SentinelRedissonManager();
break;
case "cluster":
redissonManager = new ClusterRedissonManager();
break;
default:
throw new IllegalStateException("Unexpected value: " + redisConfiguration.deployType);
}
this.redissonClient = redissonManager.initRedissonClient(redisConfiguration);
}
использовал один здесьрежим стратегии, который можно инициализировать в соответствии с различными методами развертывания Redis.RedissonClient.
RedisLock:
Здесь, чтобы интегрировать распределенные блокировки zookeeper и etcd, я абстрагировал класс метода шаблона AbstractLock, который реализует java.util.concurrent.locks.Lock.
Таким образом, независимо от того, какой тип распределенной блокировки будет использоваться позже, его можно определить с помощью Lock lock = new xxx().
Это отражено в следующей статье:
Есть ли лучшая реализация распределенной блокировки, чем Redis? Да, и тд!
public class RedisLock extends AbstractLock {
private RedissonClient redissonClient;
private String lockKey;
public RedisLock(RedissonClient redissonClient, String lockKey) {
this.redissonClient = redissonClient;
this.lockKey = lockKey;
}
@Override
public void lock() {
redissonClient.getLock(lockKey).lock();
}
//...略
@Override
public void unlock() {
redissonClient.getLock(lockKey).unlock();
}
//...
}
запрос API:
@RequestMapping("/v3/pview")
public String incrPviewWithDistributedLock() {
Lock lock = new RedisLock(redissonClient, lockKey);
try {
//加锁
lock.lock();
int oldPview = Integer.valueOf((String) redissonClient.getBucket("pview", new StringCodec()).get());
//执行业务 阅读量增加1
int newPview = oldPview + 1;
redissonClient.getBucket("pview", new StringCodec()).set(String.valueOf(newPview));
LOGGER.info("{} 成功获得锁,阅读量加1,当前阅读量:{}", Thread.currentThread().getName(), newPview);
} catch (Exception e) {
e.printStackTrace();
} finally {
//释放锁
lock.unlock();
}
return port + " increase pview end!";
}
Выполнить результаты испытаний под давлением:
Судя по результатам, проблем нет.
Анализ исходного кода блокировки RedissonLock
посмотриRedissonLock
Закрытый исходный код:
<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
this.internalLockLeaseTime = unit.toMillis(leaseTime);
return this.evalWriteAsync(this.getName(), LongCodec.INSTANCE, command,
"if (redis.call('exists', KEYS[1]) == 0) then " +
"redis.call('hincrby', 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.singletonList(this.getName()),
this.internalLockLeaseTime,
this.getLockName(threadId));
}
Сценарий Lua выполняется, и причиной использования сценария Lua является
- атомарная операция. Redis будетВыполнить весь скрипт как единое целое, не прерывается. Может использоваться для пакетного обновления, пакетной вставки
- Уменьшить нагрузку на сеть.Объединение нескольких операций Redis в один скрипт, уменьшить задержку сети
- повторное использование кода. Сценарий, отправленный клиентом, может храниться в Redis, и другие клиенты могут вызывать его в соответствии с идентификатором сценария.
Здесь используются несколько команд Redis:
-
hincrby
HINCRBY key field increment
Добавьте шаг приращения к значению поля field в ключе хеш-таблицы.
Приращение также может быть отрицательным, что эквивалентно операции вычитания в заданном поле.
Если ключ не существует, создается новая хеш-таблица и выполняется команда HINCRBY.
Если поле field не существует, то перед выполнением команды значение поля инициализируется равным 0.
возвращаемое значение:
После выполнения команды HINCRBY значение поля field в ключе хеш-таблицы.
-
pexpire
PEXPIRE key milliseconds
эта команда иEXPIREКоманда работает аналогично, но начинается смиллисекундаУстанавливает время жизни ключа в единицах, а не в секундах, как команда EXPIRE.
возвращаемое значение:
Установить успешно, вернуть 1
ключ не существует или установка не удалась, вернуть 0
-
hexists
HEXISTS key field
Проверьте, существует ли данное поле field в ключе хеш-таблицы.
возвращаемое значение:
Возвращает 1, если хеш-таблица содержит данное поле.
Возвращает 0, если хеш-таблица не содержит заданного поля или ключ не существует.
-
pttl
PTTL key
Эта команда похожа на команду TTL, но начинается смиллисекундаединица измеренияВозвращает оставшееся время жизни для ключавместо секунд, как в команде TTL.
возвращаемое значение:
Возвращает -2, если ключ не существует.
Возвращает -1, если ключ существует, но оставшееся время жизни не установлено. В противном случае возвращает оставшееся время жизни ключа в миллисекундах.
Теперь снова посмотрите на этот Lua-скрипт,
- еслиKEYS[1]не существует,
затем выполнитьhincrby KEYS[1] ARGV[2] 1
,
Указывает, что установлен хеш, ключ которого KEYS[1], и k=ARGV[2], v=1,
(потому чтоhincrby: Если поле field не существует, то значение поля инициализируется равным 0 перед выполнением команды. )
затем выполнитьpexpire KEYS[1] ARGV[1]
Установить срок действия
- еслиKEYS[1]существует,
воплощать в жизньhincrby KEYS[1] ARGV[2] 1
Выражается как значение поля field в хеш-таблице key плюс 1, то есть повторный вход в блокировку;
Затем установите срок годности.
Красный замок RedisRedLock
Предыдущее решение, похоже, решает проблему использования общих ресурсов в распределенной системе, но этоСоздан при условии, что Redis никогда не выйдет из строя.
еслизамокиспользоватьRedis Sentinelрежиме, есть время простоя узла:
- Клиент получает блокировку через MasterA, время ожидания блокировки составляет 20 секунд;
- До того, как наступит время истечения блокировки (то есть не прошло более 20 секунд после добавления блокировки) MasterA не работает;
- Sentinel подтягивает один из узлов Slave, чтобы он стал MasterB;
- MasterB не находит блокировки, она также блокируется;
- MasterB также дает сбой во время истечения срока действия блокировки, а Sentinel подтягивает MasterC;
- MasterC заблокирован...
Наконец-то на этом замке есть 3 экземпляра одновременно! Это совершенно невыносимо!
Редис дает намRedLockРешение для красного замка.
Шаги алгоритма RedLock
В распределенной среде Redis мы предполагаем, что существует N мастеров Redis. Эти узлы полностью независимы друг от друга, и в них нет репликации ведущий-подчиненный или других механизмов координации кластера.
Возьмем в качестве примера 5 узлов Redis, это разумная настройка, поэтому нам нужно запустить эти экземпляры на 5 машинах или 5 виртуальных машинах, чтобы гарантировать, что они не выйдут из строя одновременно (ниже мы используем 1 чтобы открыть 5 пример для моделирования).
Чтобы получить блокировку, клиент должен сделать следующее:
- Получить текущее время Unix в миллисекундах.
- Попробуйте последовательно из N экземпляров, используя один и тот же ключ и случайное значениеполучить замок.
На шаге 2, когдаПри установке блокировки для Redis клиент должен установить сетевое подключение и время ожидания ответа, которое должно быть меньше времени истечения блокировки..
Например, если ваша блокировка автоматически истекает через 10 секунд, время ожидания должно составлять от 5 до 50 миллисекунд. Это позволяет избежать ситуации, когда серверная часть Redis зависла, а клиентская сторона все еще ожидает результата ответа.Если сервер не отвечает в течение указанного времени, клиент должен как можно скорее попробовать другой экземпляр Redis.
- Клиент использует текущее время минус время начала получения блокировки (время, записанное на шаге 1), чтобыПолучить время, используемое для получения блокировки.
тогда и только тогда, когда от большинства(здесь 3 узла)Когда все узлы Redis получили блокировку, а время использования меньше времени истечения срока действия блокировки, блокировка считается полученной успешно..
- Если замок получен, реальное время действия ключа равноЭффективное время минус время, затраченное на получение блокировки(Результат рассчитан на шаге 3).
- Если по какой-то причине,Не удалось получить блокировку(Блокировка не была получена как минимум в N/2+1 экземплярах Redis или время получения блокировки превысило действительное время).Клиенты должны быть разблокированы на всех экземплярах Redis.(Даже если некоторые экземпляры Redis вообще не заблокированы).
Использование RedLock для реализации распределенных блокировок
Здесь открыто 5 экземпляров Redis, и RedLock используется для реализации распределенных блокировок.
Список экземпляров Redis, используемых распределенными блокировками:
# Redis分布式锁使用的redis实例
192.168.2.11 : 6479
192.168.2.11 : 6579
192.168.2.11 : 6679
192.168.2.11 : 6779
192.168.2.11 : 6889
Для удобства храните данные на одноузловом экземпляре Redis (он также может быть master-slave, sentinel, cluster):
# 存储数据用的redis
192.168.2.11 : 6379
Реализация кода красного замка:
// ============== 红锁 begin 方便演示才写在这里 可以写一个管理类 ==================
public static RLock create(String redisUrl, String lockKey) {
Config config = new Config();
//未测试方便 密码写死
config.useSingleServer().setAddress(redisUrl).setPassword("redis123");
RedissonClient client = Redisson.create(config);
return client.getLock(lockKey);
}
RedissonRedLock redissonRedLock = new RedissonRedLock(
create("redis://192.168.2.11:6479", "lock1"),
create("redis://192.168.2.11:6579", "lock2"),
create("redis://192.168.2.11:6679", "lock3"),
create("redis://192.168.2.11:6779", "lock4"),
create("redis://192.168.2.11:6889", "lock5")
);
@RequestMapping("/v4/pview")
public String incrPview() {
Lock lock = new RedisRedLock(redissonRedLock);
try {
//加锁
lock.lock();
//执行业务 阅读量增加1
int oldPview = Integer.valueOf((String) redissonClient.getBucket("pview", new StringCodec()).get());
int newPview = oldPview + 1;
redissonClient.getBucket("pview", new StringCodec()).set(String.valueOf(newPview));
LOGGER.info("{} 成功获得锁,阅读量加1,当前阅读量:{}", Thread.currentThread().getName(), newPview);
} catch (Exception e) {
e.printStackTrace();
} finally {
//释放锁
lock.unlock();
}
return port + " increase pview end!";
}
Результаты испытаний под давлением:
Получилось идеально!
Таким образом, мы используем красную блокировку Redis RedLock для реализации распределенной блокировки.
Красный код блокировки Redis, реализованный на основе Reddisson, находится в классе
org.redisson.RedissonMultiLock
середина:
выше.
Полный код этой статьи:GitHub.com/Ответственность маленького белого дракона/День третий…
Первый публичный аккаунт:линейный барьерПриветствую старое железо внимание.