проблема
(1) Как Redis реализует распределенные блокировки?
(2) Каковы преимущества распределенных блокировок redis?
(3) Каковы недостатки распределенных блокировок redis?
(4) Есть ли готовые колеса для redis для реализации распределенных блокировок?
Введение
Redis (полное название: Remote Dictionary Server Remote Dictionary Service) — это база данных с открытым исходным кодом типа журнала, ключ-значение, написанная на языке ANSI C, поддерживающая сеть, память на основе и постоянство, а также предоставляющая API-интерфейсы на нескольких языках.
В этой главе мы познакомим вас с тем, как реализовать распределенные блокировки на основе redis, и объясним эволюционную историю его внедрения от начала до конца, чтобы вы могли ясно объяснить приближающийся (внезапный) долгий (вы) к (экзамен) redis. раздавал замки во время интервью. ) Пульс (официальный).
Условия реализации блокировок
Основываясь на предыдущих знаниях о блокировках (распределенных блокировках), мы знаем, что есть три условия для реализации блокировок:
(1) State (общая) переменная, которая имеет состояние. Значение этого состояния определяет, было ли оно заблокировано. В ReentrantLock это реализуется путем управления значением состояния, а в ZookeeperLock — путем управления дочерними узлами. ;
(2) очередь, которая используется для хранения потока, достигается с помощью AQS ReentrantLock в очереди, это достигается путем упорядочивания дочерних узлов в ZookeeperLock;
(3) пробуждение, пробуждение следующего ожидающего потока после снятия блокировки предыдущим потоком и автоматическое пробуждение следующего потока при освобождении очереди AQS в ReentrantLock, что достигается в ZookeeperLock с помощью его механизма мониторинга;
Обязательны ли три вышеуказанных условия?
На самом деле, единственное необходимое условие для реализации блокировки - это первое, управление общей переменной, если значение общей переменной равно null, установить для нее значение (в java вы можете использовать CAS для работы с общей переменной в процессе), если общая переменная имеет значение. Затем несколько раз проверьте, имеет ли она значение (повторная попытка), и установите значение общей переменной обратно в нуль после выполнения логики в блокировке.
Грубо говоря, пока есть место для хранения этой общей переменной, необходимо следить за тем, чтобы во всей системе была только одна копия (несколько процессов).
Это также ключ к реализации распределенных блокировок в Redis [Эта статья изначально была создана общедоступной учетной записью «Tongge Reading Source Code»].
История развития распределенной блокировки Redis
Эволюционная история один - набор
Поскольку выше сказано, что для реализации распределенных блокировок нужно только контролировать общие переменные на месте, то как мы можем контролировать эту общую переменную в Redis?
Во-первых, мы знаем, что основные команды Redis — это get/set/del. Можно ли реализовать распределенные блокировки с помощью этих трех команд? Конечно.
до получения блокировкиget lock_user_1
Посмотрите, нет ли этой защелки, а если ее нет, тоset lock_user_1 value
, если он существует, подождите некоторое время и повторите попытку, а затем удалите блокировку после завершения последнего использования.del lock_user_1
Вот и все.
Однако у этой схемы есть проблема: если блокировки в начале нет, и два потока идут на получение одновременно, то оба они в это время возвращают ноль (nil), а затем оба потока идут на установку , а затем возникает ошибка Проблема в том, что оба потока могут установить успешно, что означает, что оба потока получили одну и ту же блокировку.
Таким образом, это решение невозможно!
Эволюционная история II - setnx
Основная причина, по которой приведенная выше схема невозможна, заключается в том, что несколько потоков могут быть успешно установлены одновременно, поэтому позжеsetnx
эта команда, этоset if not exist
Аббревиатура для , то есть установить, если он не существует.
Можно видеть, что когда setnx выполняется повторно для одной и той же клавиши, только первый раз может быть успешным.
Поэтому второй вариант — использоватьsetnx lock_user_1 value
Команда, если она возвращает 1, это означает, что блокировка прошла успешно, если она возвращает 0, это означает, что сначала успешно выполнились другие потоки, затем подождите некоторое время и повторите попытку, и, наконец, используйте то же самое.del lock_user_1
Освободите замок.
Однако и у этого решения есть проблема: что делать, если клиент, получивший блокировку, отключен? Разве этот замок никогда не открывается? Да, это.
Следовательно, это решение невозможно!
Эволюционная история 3 - setnx + setex
Основная причина, по которой приведенное выше решение невозможно, заключается в том, что клиент отключается после получения блокировки и не может снять блокировку.Итак, могу ли я выполнить setex сразу после setnx?
Ответ да.Версия до 2.6.12 использовала redis для реализации распределенных блокировок.Все так играли.
Таким образом, третий вариант заключается в использованииsetnx lock_user_1 value
Команда получает блокировку и немедленно ее использует.setex lock_user_1 30 value
Установите время истечения срока действия и используйте его последнимdel lock_user_1
Освободите замок.
После того, как setnx получит блокировку, выполните setex, чтобы установить время истечения срока действия, что решает проблему, заключающуюся в том, что отключение клиента не приведет к освобождению блокировки после получения блокировки с высокой вероятностью.
Однако у этого решения все еще есть проблемы: что делать, если клиент отключается до setex после setnx? Хм~ вроде бы решения нет, но эта вероятность действительно очень мала, поэтому все пользовались им в версии до 2.6.12, и проблем почти не было.
Таким образом, это решение в основном пригодно для использования, но не очень хорошо!
История развития 4 --set NX EX
Не очень хорошо, в основном из-за вышеуказанной программы SETNX / SETEX - два отдельных команда, нельзя решить после успеха бывшего клиентской проблемы отключения клиента, то две команды еще не в строке?
Да, официальные лица Redis тоже знают об этой проблеме, поэтому в версии 2.6.12 в команду set добавлены некоторые параметры:
SET key value [EX seconds] [PX milliseconds] [NX|XX]
EX, время экспирации, в секундах
PX, время экспирации, в миллисекундах
NX, не существует, если нет успеха перед настройкой
ХХ, СУЩЕСТВУЕТ СУЩЕСТВУЕТ? Если существование установлено успешно
С помощью этой команды мы больше не боимся, что клиент отключится без всякой причины [эта статья изначально была создана публичной учетной записью «Tong Ge Reading Source Code»].
Следовательно, четвертый вариант — использовать сначалаset lock_user_1 value nx ex 30
Приобретите блокировку, используйте его после приобретения блокировки и завершите его в концеdel lock_user_1
Освободите замок.
Однако есть ли проблемы с этой схемой?
Проблема конечно есть, на самом деле блокировка разблокировки тут просто выполняетсяdel lock_user_1
То есть он не проверяет, получена ли блокировка текущим клиентом.
Поэтому эта схема не идеальна.
Эволюционная история пять - случайное значение + lua скрипт
Основная причина приведенного выше решения в том, что блокировка освобождения здесь не очень уместна, значит, нет другого способа управлять потоком и блокирующим потоком, который можно настроить так, чтобы он был одним и тем же клиентом?
Указанная официальная программа Redis это:
// 加锁
SET resource_name my_random_value NX PX 30000
// 释放锁
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
При блокировке установите случайное значение, чтобы убедиться, что только текущий клиент знает это случайное значение.
При освобождении блокировки выполните сценарий lua, рассматривайте этот сценарий lua как завершенную команду, сначала проверьте, является ли значение, соответствующее блокировке, случайным значением, установленным выше, если это так, выполните del, чтобы снять блокировку, иначе он вернется непосредственно к отказу от снятия блокировки.
Мы знаем, что redis однопоточный, поэтому у get и del в этом lua-скрипте не будет проблем с параллелизмом, но вы не можете сначала получить, а потом del в java, это будет рассматриваться как две команды, будут проблемы с параллелизмом , lua-скрипт вполне так команда передается в redis вместе.
Эта схема относительно совершенна, но все же есть небольшой недостаток, то есть сколько времени экспирации выставлено, чтобы быть адекватным?
Если параметр слишком мал, возможно, что блокировка будет автоматически снята до того, как предыдущий поток завершит выполнение логики в блокировке, так что другой поток сможет получить блокировку, что приведет к проблемам параллелизма;
Если значение слишком велико, клиент будет отключен, а блокировка будет ждать долго.
Поэтому здесь возникла новая проблема: я установил время истечения меньше, но его можно автоматически продлевать, когда оно вот-вот истечет.
Эволюционная история VI - Redisson (redis2.8+)
Недостаток приведенной выше схемы в том, что время истечения срока действия не так просто понять.Хотя вы также можете запустить поток мониторинга для обработки обновления, код действительно не так просто написать.К счастью, готовый редиссон колеса уже помог мы понимаем эту логику.Просто возьми ее и используй напрямую.
Более того, redisson полностью учитывает различные проблемы, оставшиеся в процессе эволюции redis, автономный режим, дозорный режим, кластерный режим, все они были обработаны, будь то эволюция от одной машины до кластера или от дозорного до кластера. кластер, все только нужно. Вы можете просто изменить конфигурацию без изменения кода.
Распределенная блокировка, реализованная redisson, использует внутри себя алгоритм Redlock, который является официально рекомендованным алгоритмом.
Кроме того, Redisson также предоставляет множество распределенных объектов (распределенные атомарные классы), распределенный набор (распределенная карта/список/очередь и т. д.), распределенный синхронизатор (распределенный countdownLatch/семафор, распределение Stag lock (распределенный публичный замок/непубличный блокировка / блокировка чтения и записи и т. д.)
Введение в Редлок: https://redis.io/topics/distlock
Введение в Redisson: https://github.com/redisson/redisson/wiki
Код
Поскольку первые пять решений устарели, Tong Ge здесь ленив, поэтому я не буду реализовывать их по одному, давайте посмотрим непосредственно на последнюю реализацию redisson.
файл pom.xml
Добавьте весну Redis и Redisson, я использую SpringBoot 2.1.6, SpringBoot 1.x версия себя, проверьте вышеуказанный GitHub можно найти метод.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-data-21</artifactId>
<version>3.11.0</version>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.11.0</version>
</dependency>
Application.yml Файл
Чтобы настроить информацию о подключении Redis, Brother Tong предлагает три способа.
spring:
redis:
# 单机模式
#host: 192.168.1.102
#port: 6379
# password: <your passowrd>
timeout: 6000ms # 连接超时时长(毫秒)
# 哨兵模式 【本篇文章由公众号“彤哥读源码”原创】
# sentinel:
# master: <your master>
# nodes: 192.168.1.101:6379,192.168.1.102:6379,192.168.1.103:6379
# 集群模式(三主三从伪集群)
cluster:
nodes:
- 192.168.1.102:30001
- 192.168.1.102:30002
- 192.168.1.102:30003
- 192.168.1.102:30004
- 192.168.1.102:30005
- 192.168.1.102:30006
Интерфейс шкафчика
Определите интерфейс Locker.
public interface Locker {
void lock(String key, Runnable command);
}
Класс реализации RedisLocker
Для получения блокировки используйте RedissonClient напрямую. Обратите внимание, что здесь нет необходимости отдельно настраивать bean-компонент RedissonClient. Фреймворк redisson автоматически сгенерирует экземпляр RedissonClient в соответствии с конфигурацией. О том, как это реализовано, мы поговорим позже.
@Component
public class RedisLocker implements Locker {
@Autowired
private RedissonClient redissonClient;
@Override
public void lock(String key, Runnable command) {
RLock lock = redissonClient.getLock(key);
try {
// 【本篇文章由公众号“彤哥读源码”原创】
lock.lock();
command.run();
} finally {
lock.unlock();
}
}
}
тестовый класс
Начните 1000 потоков, распечатайте предложение внутри каждого потока, а затем сон в течение 1 секунды.
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
public class RedisLockerTest {
@Autowired
private Locker locker;
@Test
public void testRedisLocker() throws IOException {
for (int i = 0; i < 1000; i++) {
new Thread(()->{
locker.lock("lock", ()-> {
// 可重入锁测试
locker.lock("lock", ()-> {
System.out.println(String.format("time: %d, threadName: %s", System.currentTimeMillis(), Thread.currentThread().getName()));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
});
}, "Thread-"+i).start();
}
System.in.read();
}
}
результат операции:
Видно, что примерно через 1000 мс выводится предложение, указывающее на то, что блокировка доступна и ее можно повторно использовать.
time: 1570100167046, threadName: Thread-756
time: 1570100168067, threadName: Thread-670
time: 1570100169080, threadName: Thread-949
time: 1570100170093, threadName: Thread-721
time: 1570100171106, threadName: Thread-937
time: 1570100172124, threadName: Thread-796
time: 1570100173134, threadName: Thread-944
time: 1570100174142, threadName: Thread-974
time: 1570100175167, threadName: Thread-462
time: 1570100176180, threadName: Thread-407
time: 1570100177194, threadName: Thread-983
time: 1570100178206, threadName: Thread-982
...
RedissonAutoConfiguration
Я лишь сказал, что RedissonClient не нуждается в настройке, на самом деле он настраивается автоматически в RedissonAutoConfiguration, давайте вкратце рассмотрим его исходный код, в основном рассматривая метод redisson():
@Configuration
@ConditionalOnClass({Redisson.class, RedisOperations.class})
@AutoConfigureBefore(RedisAutoConfiguration.class)
@EnableConfigurationProperties({RedissonProperties.class, RedisProperties.class})
public class RedissonAutoConfiguration {
@Autowired
private RedissonProperties redissonProperties;
@Autowired
private RedisProperties redisProperties;
@Autowired
private ApplicationContext ctx;
@Bean
@ConditionalOnMissingBean(name = "redisTemplate")
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate<Object, Object>();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
@Bean
@ConditionalOnMissingBean(StringRedisTemplate.class)
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
@Bean
@ConditionalOnMissingBean(RedisConnectionFactory.class)
public RedissonConnectionFactory redissonConnectionFactory(RedissonClient redisson) {
return new RedissonConnectionFactory(redisson);
}
@Bean(destroyMethod = "shutdown")
@ConditionalOnMissingBean(RedissonClient.class)
public RedissonClient redisson() throws IOException {
Config config = null;
Method clusterMethod = ReflectionUtils.findMethod(RedisProperties.class, "getCluster");
Method timeoutMethod = ReflectionUtils.findMethod(RedisProperties.class, "getTimeout");
Object timeoutValue = ReflectionUtils.invokeMethod(timeoutMethod, redisProperties);
int timeout;
if(null == timeoutValue){
// 超时未设置则为0
timeout = 0;
}else if (!(timeoutValue instanceof Integer)) {
// 转毫秒
Method millisMethod = ReflectionUtils.findMethod(timeoutValue.getClass(), "toMillis");
timeout = ((Long) ReflectionUtils.invokeMethod(millisMethod, timeoutValue)).intValue();
} else {
timeout = (Integer)timeoutValue;
}
// 看下是否给redisson单独写了一个配置文件
if (redissonProperties.getConfig() != null) {
try {
InputStream is = getConfigStream();
config = Config.fromJSON(is);
} catch (IOException e) {
// trying next format
try {
InputStream is = getConfigStream();
config = Config.fromYAML(is);
} catch (IOException e1) {
throw new IllegalArgumentException("Can't parse config", e1);
}
}
} else if (redisProperties.getSentinel() != null) {
// 如果是哨兵模式
Method nodesMethod = ReflectionUtils.findMethod(Sentinel.class, "getNodes");
Object nodesValue = ReflectionUtils.invokeMethod(nodesMethod, redisProperties.getSentinel());
String[] nodes;
// 看sentinel.nodes这个节点是列表配置还是逗号隔开的配置
if (nodesValue instanceof String) {
nodes = convert(Arrays.asList(((String)nodesValue).split(",")));
} else {
nodes = convert((List<String>)nodesValue);
}
// 生成哨兵模式的配置
config = new Config();
config.useSentinelServers()
.setMasterName(redisProperties.getSentinel().getMaster())
.addSentinelAddress(nodes)
.setDatabase(redisProperties.getDatabase())
.setConnectTimeout(timeout)
.setPassword(redisProperties.getPassword());
} else if (clusterMethod != null && ReflectionUtils.invokeMethod(clusterMethod, redisProperties) != null) {
// 如果是集群模式
Object clusterObject = ReflectionUtils.invokeMethod(clusterMethod, redisProperties);
Method nodesMethod = ReflectionUtils.findMethod(clusterObject.getClass(), "getNodes");
// 集群模式的cluster.nodes是列表配置
List<String> nodesObject = (List) ReflectionUtils.invokeMethod(nodesMethod, clusterObject);
String[] nodes = convert(nodesObject);
// 生成集群模式的配置
config = new Config();
config.useClusterServers()
.addNodeAddress(nodes)
.setConnectTimeout(timeout)
.setPassword(redisProperties.getPassword());
} else {
// 单机模式的配置
config = new Config();
String prefix = "redis://";
Method method = ReflectionUtils.findMethod(RedisProperties.class, "isSsl");
// 判断是否走ssl
if (method != null && (Boolean)ReflectionUtils.invokeMethod(method, redisProperties)) {
prefix = "rediss://";
}
// 生成单机模式的配置
config.useSingleServer()
.setAddress(prefix + redisProperties.getHost() + ":" + redisProperties.getPort())
.setConnectTimeout(timeout)
.setDatabase(redisProperties.getDatabase())
.setPassword(redisProperties.getPassword());
}
return Redisson.create(config);
}
private String[] convert(List<String> nodesObject) {
// 将哨兵或集群模式的nodes转换成标准配置
List<String> nodes = new ArrayList<String>(nodesObject.size());
for (String node : nodesObject) {
if (!node.startsWith("redis://") && !node.startsWith("rediss://")) {
nodes.add("redis://" + node);
} else {
nodes.add(node);
}
}
return nodes.toArray(new String[nodes.size()]);
}
private InputStream getConfigStream() throws IOException {
// 读取redisson配置文件
Resource resource = ctx.getResource(redissonProperties.getConfig());
InputStream is = resource.getInputStream();
return is;
}
}
Многие конфигурации в информации, найденной в Интернете, избыточны (могут быть проблемы с версией) Очень хорошо видно исходный код, что также является преимуществом просмотра исходного кода.
Суммировать
(1) Redis имеет три режима по историческим причинам: автономный, дозорный и кластерный;
(2) Эволюционная история распределенных блокировок, реализованных с помощью redis: set -> setnx -> setnx + setex -> set nx ex (или px) -> set nx ex (или px) + скрипт lua -> redisson;
(3) Распределенные блокировки Redis имеют готовые колеса redisson, которые можно использовать;
(4) redisson также предоставляет множество полезных компонентов, таких как распределенные коллекции, распределенные синхронизаторы и распределенные объекты;
пасхальные яйца
Каковы преимущества распределенных блокировок Redis?
Ответ: 1) Большинство систем полагаются на Redis для кэширования и не должны полагаться на другие компоненты (по сравнению с zookeeper);
2) Redis может быть кластеризован для развертывания, что более надежно, чем единая точка MySQL;
3) Не будет занимать количество соединений mysql и не будет увеличивать нагрузку на mysql;
4) Redis-сообщество относительно активно, а реализация redisson более стабильна и надежна;
5) Использовать механизм истечения для решения проблемы отключения клиента, хотя и несвоевременно;
6) Есть готовые колесные редиссоны, которые можно использовать, и типы замков относительно полные;
Каковы недостатки распределенных блокировок redis?
Ответ: 1) В режиме кластера команда блокировки будет выполняться на всех мастер-нодах, большинство (2N+1) успешны и блокировка будет получена, чем больше нод, тем медленнее процесс блокировки;
2) В случае высокого параллелизма поток, не получивший блокировку, будет спать и повторять попытку.Если конкуренция за ту же блокировку будет очень жесткой, она займет много системных ресурсов;
3) Есть много ям, вызванных историческими причинами, и трудно реализовать надежную распределенную блокировку redis самостоятельно;
Короче говоря, преимущества распределенных замков redis перевешивают недостатки, и сообщество активно, поэтому большинство наших систем используют redis в качестве распределенных замков.
Рекомендуемое чтение
1,Открытие серии java-синхронизации мертвых приседаний
2,Небезопасный анализ мертвого магического класса Java
3.JMM (модель памяти Java) из мертвой серии синхронизации Java
4.Неустойчивый анализ мертвой серии синхронизации Java
5.Синхронный анализ мертвой серии синхронизации Java
6.Напишите блокировку самостоятельно в серии «Синхронизация Java»
7.Начало AQS в серии синхронизации Java
9,ReentrantLock Анализ исходного кода мертвой серии синхронизации Java (2) — условная блокировка
10.ReentrantLock VS синхронизирован в серии синхронизации java
11Анализ исходного кода ReentrantReadWriteLock мертвой серии синхронизации Java
12.Анализ исходного кода семафора серии Dead Java Synchronization
13.Анализ исходного кода CountDownLatch серии Dead Java Synchronization
14.Заключительная глава AQS в серии о синхронизации Java.
15,Анализ исходного кода StampedLock мертвой серии синхронизации Java
16.Анализ исходного кода CyclicBarrier мертвой серии синхронизации java
17.Анализ исходного кода Phaser для серии синхронизации Java с мертвым стуком
18.MakeLock Java Synchronization Series Mysql распределенный замок
19.Zookeeper раздал блокировку мертвой серии синхронизации java
Добро пожаловать, чтобы обратить внимание на мою общедоступную учетную запись «Брат Тонг читает исходный код», просмотреть больше статей в серии исходного кода и поплавать в океане исходного кода с братом Тонгом.