Всем привет, иду, оригинальный текст был впервые опубликован в паблике проспекта программирования. Спасибо, что открыли эту статью, пожалуйста, прочитайте ее внимательно. Сегодня мы поговорим о реальном сценарии использования Redis при разработке, который является знаменитой распределенной блокировкой.
Что такое распределенная блокировка?
Когда мы изучаем Java, мы все знаем концепцию блокировок, такую как синхронизированная блокировка, основанная на реализации JVM, и набор блокировок механизма блокировки на уровне кода, предоставляемый jdk.Мы часто используем эти две блокировки в параллельном программировании, чтобы гарантировать, что код находится в многопоточной среде корректность работы. Однако эти механизмы блокировки неприменимы в распределенных сценариях. Причина в том, что в распределенных бизнес-сценариях наш код выполняется на разных JVM или даже на разных машинах, а синхронизацию и блокировку можно использовать только в одной и той же среде JVM. Итак, в настоящее время вам нужно использовать распределенные блокировки.
Например, теперь есть сцена, где вы получаете потребительские купоны в час (из-за эпидемии Alipay недавно открылась для получения потребительских купонов в 8:00 и 12:00 в час), существует фиксированное количество потребительских купонов. , первый пришел, первый обслужен Теперь развернуто несколько онлайн-сервисов, и общая архитектура выглядит следующим образом:
Так что на данный момент нам приходится использовать распределенные блокировки для обеспечения корректности доступа к общим ресурсам.
Зачем использовать распределенные блокировки?
Предполагая, что распределенные блокировки не используются, давайте посмотрим, может ли это гарантировать синхронизация? На самом деле не может, давайте продемонстрируем это.
Ниже я написал простой проект springboot, чтобы имитировать этот сценарий захвата потребительских купонов.Код очень простой.Это примерно означает сначала получить оставшиеся потребительские купоны от Redis, а затем решить, что он больше 0, а затем вычесть единицу для имитации быть захваченным пользователем. , а затем вычесть один, а затем изменить оставшееся количество потребительских купонов Redis, успешно распечатать вычет и сколько осталось, иначе вычет не будет выполнен, и вы его не получите. Весь код обернут синхронизацией, а количество инвентаря, установленное Redis, равно 50.
//假设库存编号是00001
private String key = "stock:00001";
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 扣减库存 synchronized同步锁
*/
@RequestMapping("/deductStock")
public String deductStock(){
synchronized (this){
//获取当前库存
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get(key));
if(stock>0){
int afterStock = stock-1;
stringRedisTemplate.opsForValue().set(key,afterStock+"");//修改库存
System.out.println("扣减库存成功,剩余库存"+afterStock);
}else {
System.out.println("扣减库存失败");
}
}
return "ok";
}
Затем запустите два проекта springboot, порты 8080, 8081, а затем настройте балансировку нагрузки в nginx.
upstream redislock{
server 127.0.0.1:8080;
server 127.0.0.1:8081;
}
server {
listen 80;
server_name 127.0.0.1;
location / {
root html;
index index.html index.htm;
proxy_pass http://redislock;
}
}
Затем используйте инструмент для проверки давления jmeter, чтобы проверить
Затем мы смотрим на вывод консоли, мы видим, что мы запускаем два веб-экземпляра, многие из одинаковых купонов захватываются разными потоками, что доказывает, что синхронизация в таком случае не работает, поэтому нам нужно использовать распределенные блокировки для обеспечить правильность ресурсов.
Как реализовать распределенные блокировки с помощью Redis?
Прежде чем внедрять распределенные блокировки, мы сначала рассмотрим, как их реализовать и какие функции блокировок должны быть реализованы.
1. Распределенные функции (экземпляры, развернутые на нескольких машинах, могут получить доступ к замку)
2. Эксклюзивность (одновременно удерживать блокировку может только один поток)
3. Функция автоматического освобождения с течением времени (потоку, удерживающему блокировку, необходимо дать определенное максимальное время для удержания блокировки, чтобы предотвратить смерть потока и невозможность освободить блокировку и вызвать взаимоблокировку)
4....
Основываясь на перечисленных выше основных характеристиках, которыми должны обладать распределенные блокировки, давайте подумаем, как использовать Redis?
1. Поддерживается первая распределенная функция Redis, и несколько экземпляров могут быть объединены с одним Redis.
2. Вторая эксклюзивность, то есть реализация эксклюзивной блокировки, может быть реализована с помощью команды setnx Redis.
3. Третья функция автоматического выпуска тайм-аута, Redis может установить время истечения срока действия для определенного ключа.
4. Снимите распределенную блокировку после выполнения
Время науки Команды Redis SetnxКоманда Redis Setnx (SET, если не существует) устанавливает указанное значение для ключа, когда указанный ключ не существует.грамматикаОсновной синтаксис команды redis Setnx следующий: redis 127.0.0.1:6379 > ЗНАЧЕНИЕ ИМЯ_КЛЮЧА_SETNXДоступные версии:>= 1.0.0возвращаемое значение: установить успешно, вернуть 1, установить не удалось, вернуть 0
@RequestMapping("/stock_redis_lock")
public String stock_redis_lock(){
//底层使用setnx命令
Boolean aTrue = stringRedisTemplate.opsForValue().setIfAbsent(lock_key, "true");
stringRedisTemplate.expire(lock_key,10, TimeUnit.SECONDS);//设置过期时间10秒
if (!aTrue) {//设置失败则表示没有拿到分布式锁
return "error";//这里可以给用户一个友好的提示
}
//获取当前库存
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get(key));
if(stock>0){
int afterStock = stock-1;
stringRedisTemplate.opsForValue().set(key,afterStock+"");
System.out.println("扣减库存成功,剩余库存"+afterStock);
}else {
System.out.println("扣减库存失败");
}
stringRedisTemplate.delete(lock_key);//执行完毕释放分布式锁
return "ok";
}
По-прежнему установите количество запасов на 50, давайте снова проверим его с помощью jmeter, изменим тестовый адрес jmeter на 127.0.0.1/stock_redis_lock и снова проверим с теми же настройками.
После 5 тестов грязных данных нет.Измените время отправки на 0, и после 5 тестов проблем не будет.Затем измените количество потоков на 600 и время на 0.Цикл повторяется 4 раза, и он это нормально, чтобы проверить несколько раз.
Приведенный выше код для реализации распределенных блокировок уже является относительно зрелой реализацией распределенных блокировок, которой достаточно для большинства компаний-разработчиков программного обеспечения. Но в приведенном выше коде еще есть место для оптимизации, например:
1) Исключения в приведенном выше коде мы не рассматривали.На самом деле код не такой простой.Может быть много других сложных операций,и могут возникать исключения,поэтому код снятия блокировки нужно поместить в блок finally Это гарантирует, что даже если код выдаст исключение, код, освобождающий блокировку, все равно будет выполняться.
2) Кроме того, вы заметили, что код для получения и установки времени истечения нашего распределенного кода блокировки выше является двухэтапной операцией в строках 4 и 5, то есть неатомарной операцией, которая, возможно, только что была выполнена Строка 4 не успела выполнить строку 5. Машина зависает, поэтому для блокировки не установлено время ожидания, и другие потоки не могут получить ее, если не требуется ручное вмешательство, так что это шаг для оптимизации, и Redis также обеспечивает атомарные операции. , то есть значение ключа SET EX секунд NX
Время наукиSET значение ключа [EX секунд] [PX миллисекунд] [NX|XX] Свяжите значение строкового значения с ключомнеобязательный параметрНачиная с Redis 2.6.12, поведение команды SET можно изменить с помощью ряда параметров:
- EX second : Установите время действия ключа в секундах. Значение ключа SET EX секунда имеет тот же эффект, что и второе значение ключа SETEX
- PX миллисекунды: установите время истечения ключа в миллисекунды миллисекунды. Значение ключа SET в миллисекундах PX имеет тот же эффект, что и значение в миллисекундах ключа PSETEX.
- NX : Установите ключ, только если ключ не существует. Значение ключа SET NX имеет тот же эффект, что и значение ключа SETNX.
- XX : Установите ключ только тогда, когда ключ уже существует.
StringRedisTemplate SpringBoot также имеет соответствующую реализацию метода, а именно:
//假设库存编号是00001
private String key = "stock:00001";
private String lock_key = "lock_key:00001";
@Autowired
private StringRedisTemplate stringRedisTemplate;
@RequestMapping("/stock_redis_lock")
public String stock_redis_lock() {
try {
//原子的设置key及超时时间
Boolean aTrue = stringRedisTemplate.opsForValue().setIfAbsent(lock_key, "true", 30, TimeUnit.SECONDS);
if (!aTrue) {
return "error";
}
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get(key));
if (stock > 0) {
int afterStock = stock - 1;
stringRedisTemplate.opsForValue().set(key, afterStock + "");
System.out.println("扣减库存成功,剩余库存" + afterStock);
} else {
System.out.println("扣减库存失败");
}
} catch (NumberFormatException e) {
e.printStackTrace();
} finally {
//避免死锁
stringRedisTemplate.delete(lock_key);
}
return "ok";
}
Это идеальная реализация? Что ж, для сценариев с низкими требованиями к параллелизму или небольшим параллелизмом это можно реализовать. Однако для таких сценариев, как паническая покупка и секкилл, когда трафик велик, сетевая карта сервера, дисковый ввод-вывод и загрузка ЦП могут достичь предела, поэтому время ответа сервера на запрос неизбежно станет намного медленнее, чем обычно, затем Предположим, что время ожидания только что установленной блокировки составляет 10 секунд. Если поток по какой-то причине не может выполнить блокировку в течение 10 секунд после получения блокировки, блокировка будет недействительной. В это время другие потоки будут вытеснять распределенный поток. Выполните бизнес-логику, и после того, как предыдущий поток будет выполнен, будет выполнен код для снятия блокировки в finally, и будет снята блокировка потока, который удерживает распределенную блокировку. удерживание блокировки не закончило выполнение, тогда другие потоки имеют возможность снова получить блокировку... Таким образом, вся распределенная блокировка не удастся, что будет иметь неожиданные последствия. На рисунке ниже имитируется этот сценарий.
Итак, чтобы подвести итог этой проблеме, это связано с тем, что время истечения блокировки установлено неправильно или время выполнения кода по какой-то причине больше, чем время истечения срока действия блокировки, что приводит к проблемам параллелизма, и блокировка освобождается другими потоками. , так что распределенная блокировка хаотична. Короче говоря, есть две проблемы, 1) ваша собственная блокировка снимается другими 2) тайм-аут блокировки не может быть продлен.
Первую проблему решить легко: при установке распределенной блокировки мы генерируем уникальную строку в текущем потоке и устанавливаем значение в это уникальное значение, а затем переходим к блоку finally, чтобы определить, что значение текущей блокировки является такой же, как тот, который мы установили сами.Выполните удаление следующим образом:
String uuid = UUID.randomUUID().toString();
try {
//原子的设置key及超时时间,锁唯一值
Boolean aTrue = stringRedisTemplate.opsForValue().setIfAbsent(lock_key,uuid,30,TimeUnit.SECONDS);
//...
} finally {
//是自己设置的锁再执行delete
if(uuid.equals(stringRedisTemplate.opsForValue().get(lock_key))){
stringRedisTemplate.delete(lock_key);//避免死锁
}
}
Как только проблема решена (представьте, что не так с приведенным выше кодом, мы поговорим об этом через мгновение), время ожидания блокировки очень критично, оно не может быть слишком большим или слишком маленьким, что требует оценки времени выполнения. бизнес-кода, например, установить значение 10 секунд, 20 секунд. Даже если ваша блокировка устанавливает правильный тайм-аут, приведенный выше анализ неизбежен, потому что код по какой-то причине не выполняется в течение нормального времени оценки, поэтому решение в это время состоит в том, чтобы дать блокировке непрерывный тайм-аут. Общая идея состоит в том, что бизнес-поток запускает отдельный поток и регулярно проверяет, существует ли еще распределенная блокировка, установленная бизнес-потоком.Если она существует, это означает, что бизнес-поток не закончил выполнение, поэтому период ожидания блокировки Если блокировка больше не существует, то после выполнения бизнес-потока он завершается сам.
Логика **"блокировки и продления жизни"** действительно немного сложна. Слишком много вопросов, которые нужно учитывать. Если вы не обратите внимание, будут ошибки. Не смотрите на приведенный выше код реализации распределенных блокировок и не думайте, что его очень просто реализовать. пример,
1) Установка блокировки и установка времени истечения являются неатомарными операциями, которые могут привести к взаимоблокировке.
2) Остался один из вышеперечисленных,В блоке finally определите, установлена ли блокировка вами, и если да, удалите блокировку, эти два шага не являются атомарными. Если предположить, что служба зависает сразу после того, как она признана истинной, код для удаления блокировки не будет выполнен, что приведет к взаимоблокировке. Даже если время истечения срока действия установлено, он не будет выполнен. истекает в течение этого периода. Так что здесь тоже есть на что обратить внимание.Для обеспечения атомарных операций в Redis предусмотрена функция выполнения Lua-скриптов для обеспечения атомарности операций.Как их использовать не буду расширяться.
Поэтому реализация логики «запирания и продления жизни» все еще немного сложна, к счастью, на рынке уже есть фреймворк с открытым исходным кодом, который поможет нам это реализовать, то есть Redisson.
Принцип реализации распределенного замка Redisson
Принцип реализации:
1. Во-первых, Redisson попытается заблокировать. Принцип блокировки также заключается в использовании атомарной блокировки команды setnx, аналогичной Redis. Если блокировка будет успешной, дочерний поток будет открыт внутри. 2. Дочерний поток в основном отвечает за мониторинг.Это по сути таймер.Он регулярно следит за тем,удерживает ли основной поток блокировку.Если блокировка удерживается,то время блокировки будет отложено,иначе поток завершится. 3. Если блокировка не удалась, спин продолжает пытаться заблокировать 4. После выполнения кода основной поток активно снимает блокировку
Тогда давайте посмотрим, как выглядит код после использования Redisson.
1. Сначала добавьте координаты maven Redisson в файл pom.xml.
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.12.5</version>
</dependency>
2. Нам нужно получить этот объект Redisson и настроить Bean следующим образом
@SpringBootApplication
public class RedisLockApplication {
public static void main(String[] args) {
SpringApplication.run(RedisLockApplication.class, args);
}
@Bean
public Redisson redisson(){
Config config = new Config();
config.useSingleServer().setAddress("redis://localhost:6379")
.setDatabase(0);
return (Redisson) Redisson.create(config);
}
}
3. Затем мы получаем экземпляр Redisson и используем его API для блокировки и снятия блокировки.
//假设库存编号是00001
private String key = "stock:00001";
private String lock_key = "lock_key:00001";
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 使用Redisson实现分布式锁
* @return
*/
@RequestMapping("/stock_redisson_lock")
public String stock_redisson_lock() {
RLock redissonLock = redisson.getLock(lock_key);
try {
redissonLock.lock();
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get(key));
if (stock > 0) {
int afterStock = stock - 1;
stringRedisTemplate.opsForValue().set(key, afterStock + "");
System.out.println("扣减库存成功,剩余库存" + afterStock);
} else {
System.out.println("扣减库存失败");
}
} catch (NumberFormatException e) {
e.printStackTrace();
} finally {
redissonLock.unlock();
}
return "ok";
}
Посмотрите на API, предоставляемый распределенной блокировкой Redisson, он очень простой? Подобно тому, как параллелизм Java становится механизмом блокировки в AQS, получите RedissonLock следующим образом.
RLock redissonLock = redisson.getLock(lock_key);
По умолчанию возвращается объект RedissonLock, который реализует интерфейс RLock, а интерфейс RLock наследует интерфейс Lock в пакете параллельного программирования JDK.
При использовании Redisson для блокировки он также предоставляет множество API, как показано ниже.
Теперь мы решили использовать самый простой метод блокировки без параметров.Просто нажмите и посмотрите на его исходный код.Мы находим окончательный код для выполнения блокировки следующим образом:
Мы видим, что нижележащий уровень использует скрипты Lua для обеспечения атомарности, использует хэш-структуру Redis для блокировки и блокировки с повторным входом.
Это выглядит проще, чем реализовать распределенные блокировки самостоятельно, но у него есть функция блокировки, которую мы написали сами, а также функция блокировки, которой у нас нет. Например, распределенная блокировка, которую он реализовал, поддерживает повторный вход и возможность ожидания, то есть, если вы попытаетесь подождать определенный период времени, она вернет false, если блокировка не будет получена. Функция redissonLock.lock(); в приведенном выше коде всегда ожидает, и внутреннее вращение пытается заблокироваться.
Distributed Java locks and synchronizers
Lock
FairLock
MultiLock
RedLock
ReadWriteLock
Semaphore
PermitExpirableSemaphore
CountDownLatch
redisson.org
Redisson предоставляет богатый API и использует большое количество внутренних сценариев Lua для обеспечения атомарных операций.Код, в котором Redisson реализует блокировки, не будет анализироваться из-за недостатка места.
Примечание: в приведенном выше примере кода, чтобы упростить демонстрацию, запрос инвентаризации Redis и изменение инвентаря не являются атомарными операциями.На самом деле, эти две операции также должны обеспечивать атомарные строки, которые могут быть реализованы с помощью функции сценария Lua, которая поставляется с редисом.
Эпилог
На этом практика распределенной блокировки Redis в основном закончена.Давайте подведем итоги распределенной блокировки Redis.
1. Если вы реализуете его самостоятельно, вам нужно обратить особое внимание на четыре момента:
- атомный замок
- Установить тайм-аут блокировки
- Тот, кто добавил блокировку, освободит ее, а атомарная операция при освобождении
- Заблокируйте жизненный вопрос.
2. Если вы используете готовую платформу распределенных замков Redisson, вам необходимо ознакомиться с ее часто используемыми API-интерфейсами и принципами реализации или выбрать другие платформы распределенных замков с открытым исходным кодом, тщательно изучить их и выбрать ту, которая соответствует потребностям вашего бизнеса.
Ссылаться на
doc.Redis fan.com/string/set. …
ууууууууууууууууууууууууууууууууууууууууууууууууууууууууу.
Если вы считаете, что эта статья полезна для вас, пожалуйста, не скупитесь на лайки.
Для получения дополнительных статей о галантерее, пожалуйста, обратите внимание на общедоступный номер:проспект программирования
В официальном аккаунте также есть много технических статей о Redis, обратите внимание
К тому же сам ходьба тоже разбираетсяОчки знаний, связанные с Redis, в виде ментальной карты, но с ней пока не разобрались, разобрались несколько дней, обратите внимание на официальный аккаунт, и через официальный аккаунт он будет проталкиваться всем~