Справочная информация: есть Лидер поставщика услуг и несколько Рабочих подписчиков на сообщения. Лидер — это программа управления очередью, которая поддерживает очередь пользователей.Когда ресурс простаивает и назначается пользователю в очереди, Лидер отправляет сообщение подписчику (сообщение имеет уникальный идентификатор).ID), подписчик выполнит специальную обработку после получения сообщения и снова отправит его во внешний интерфейс.
Проблема: внешнему интерфейсу нужно только получить сообщение, отправленное рабочим процессом, но если рабочие процессы не оценят повторную отправку сообщения, внешний интерфейс получит несколько сообщений, что повлияет на обычную бизнес-логику.
Вариант 1 (неудачный)
Когда Worker получает сообщение, попробуйте сначала из кеша Redis в соответствии с сообщениемIDЧтобы получить значение, есть два случая:
Если значение не существует, это означает, что текущее сообщение отправляется в первый раз, и вы можете продолжить выполнение программы push.Конечно, не забудьте добавить текущее сообщениеIDВставляется в кеш в качестве ключа и устанавливает срок действия, отмечая, что сообщение было отправлено.
Если значение существует, это означает, что текущее сообщение было отправлено, а процедура отправки пропущена.
Код можно написать так:
public void waitingForMsg() {
// Message Received.
String value = redisTemplate.opsForValue().get("msg_pushed_" + msgId);
if (!StringUtils.hasText(value)) {
// 当不能从缓存中读取到数据时,表示消息是第一次被推送
// 赶紧往缓存中插入一个标识,表示当前消息已经被推送过了
redisTemplate.opsForValue().set("msg_pushed_" + msgId, "1");
// 再设置一个过期时间,防止数据无限制保留
redisTemplate.expire("msg_pushed_" + msgId, 20, TimeUnit.SECONDS);
// 接下来就可以执行推送操作啦
this.pushMsgToFrontEnd();
}
}
Кажется, что проблем нет, но давайте проанализируем запрос с точки зрения Redis, чтобы увидеть, действительно ли он в порядке.
> get msg_pushed_1 # 此时Worker1尝试获取值
> get msg_pushed_1 # Worker2也没闲着,执行了这句话,并且时间找得刚刚好,就在Worker1准备插入值之前
> set msg_pushed_1 "1" # Worker1觉得消息没有被推送,插入了一个值
> set msg_pushed_1 "1" # Worker2也这么觉得,做了同样的一件事
Видите ли, по-прежнему можно отправлять сообщения во внешний интерфейс несколько раз, поэтому этот план не проходит.
Подумайте еще раз, в чем причина этой проблемы? —— То есть при выполнении команд get и set обслуживание не требуется.атомарностьоперации, в результате чего другие команды используют ее, возможно ли выполнить команды get и set как целую часть и предотвратить вставку и выполнение других команд?
Есть много решений, которые могут быть реализованы, например, блокировка ключа или добавление транзакции. Но сегодня мы обсудим другое решение — выполнение Lua-скриптов в Redis.
Вариант 2
Мы можем взглянуть на официальную документацию Redis по атомарности сценария Lua.объяснять.
Atomicity of scripts
Redis uses the same Lua interpreter to run all the commands. Also Redis guarantees that a script is executed in an atomic way: no other script or Redis command will be executed while a script is being executed. This semantic is similar to the one of MULTI / EXEC. From the point of view of all the other clients the effects of a script are either still not visible or already completed.
Общий смысл таков: мы Redis используем один и тот же интерпретатор Lua для запуска всех команд, и мы можем гарантировать, что выполнение скрипта будет атомарным. Эффект аналогичен добавлению MULTI/EXEC.
Что ж, атомарность гарантирована, так что давайте посмотрим на синтаксис записи.
> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second1) "key1"2) "key2"3) "first"4) "second"
Объяснение команд спереди назад (Arg означает аргумент):
eval: Redis выполняет команду сценария Lua, за которой следует содержимое и параметры сценария. Эта команда из2.6.0версия только поддерживается.
1. Arg: Lua-скрипт, где KEYS[] и ARGV[] — параметры, передаваемые в скрипт.
2-й аргумент: количество KEY, за которым следует n, всего n параметров, начиная с третьего параметра, будут переданы в сценарий как KEYS, которые можно прочитать в формате KEYS[1], KEYS[2].. ., индекс начинается с 1.
Remain Arg: Остальные параметры можно прочитать в сценарии в формате ARGV[1], ARGV[2]…, а индекс начинается с 1.
Выполняем содержимое скриптаre
turn {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}
Указывает, что входящие параметры возвращаются, поэтому мы видим, что параметры возвращаются без изменений.
Далее давайте взглянем на реальный бой и вызовем метод Redis в скрипте Lua.
Мы можем вызвать командную программу Redis с помощью следующих двух команд в сценарии Lua.
redis.call()
redis.pcall()
Эффект от них одинаков, но результат возврата в случае сбоя программы немного отличается.
Для использования команды точно такие же, как и в Redis:
> eval "return redis.call('set', KEYS[1], ARGV[1])" 1 foo bar
OK
> eval "return redis.call('get', KEYS[1])" 1 foo
"bar"
Разве это не очень просто, сказав так много, давайте научимся и продадим сейчас, и напишем скрипт, чтобы применить это в нашей сцене.
> eval "if redis.call('get', KEYS[1]) == false then redis.call('set', KEYS[1], ARGV[1]) redis.call('expire', KEYS[1], ARGV[2]) return 0 else return 1 end" 1 msg_push_1 "1" 10
Смысл скрипта тот же, что и вВариант первыйЛогика программы такая же, сначала определяем есть ли ключ в кеше, если нет, сохраняем ключ и его значение, и устанавливаем время истечения, и, наконец, возвращаем 0; если он есть, возвращаем 1.ПС: если даif redis.call('get', KEYS[1]) == false
Если полученный здесь результат нужно сравнить с ложным, можно посмотреть последний совет.
Выполняется в первый раз: обнаруживаем, что возвращаемое значение равно 0, и видим, что в кеш вставляется кусок данных, ключ
msg_push_1
, значение"1"
Выполните несколько раз перед аннулированием: мы обнаружим, что возвращаемое значение всегда равно 1. А через 10 секунд после первого исполнения ключ автоматически удаляется.
После переноса приведенной выше логики в наш java-код он выглядит так:
public boolean isMessagePushed(String messageId) {
Assert.hasText(messageId, "消息ID不能为空");
// 使用lua脚本检测值是否存在
String script = "if redis.call('get', KEYS[1]) == false then redis.call('set', KEYS[1], ARGV[1]) redis.call('expire', KEYS[1], ARGV[2]) return 0 else return 1 end";
// 这里使用Long类型,查看源码可知脚本返回值类型只支持Long, Boolean, List, or deserialized value type.
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setScriptText(script);
redisScript.setResultType(Long.class);
// 设置key
List<String> keyList = new ArrayList<>();
// key为消息ID
keyList.add(messageId);
// 每个键的失效时间为20秒
Long result = redisTemplate.execute(redisScript, keyList, 1, 20);
// 返回true: 已读、false: 未读
return result != null && result != 0L;
}
public void waitingForMsg() {
// Message Received.
if (!this.isMessagePushed(msgId)) {
// 返回false表示未读,接下来就可以执行推送操作啦
this.pushMsgToFrontEnd();
}
}
Tip
Это всего лишь краткое введение в использование скриптов Lua в Redis.Подробные методы использования см. в официальной документации, и есть много других вводных сведений об использовании.
Да, есть еще один на немямаСледует отметить, что речь идет о взаимном преобразовании переменных в Redis и Lua.Поскольку оно слишком многословно, то не включено в вышеизложенное.Напоследок я могу вкратце рассказать о нем.
Redis to Lua conversion table.
Redis integer reply -> Lua number
Redis bulk reply -> Lua string
Redis multi bulk reply -> Lua table (may have other Redis data types nested)
Redis status reply -> Lua table with a single ok field containing the status
Redis error reply -> Lua table with a single err field containing the error
Массовый ответ Redis Nil и многократный массовый ответ Nil -> Lua false boolean type // Это когда мы оцениваем, является ли он пустым или нет в приведенном выше скрипте
if redis.call('get', KEYS[1]) == false
, причина взятия сравнения с ложным. Ноль Redis (похожий на null) будет преобразован в ложь Lua.Lua to Redis conversion table.
Lua number -> Redis integer reply (the number is converted into an integer)
Lua string -> Redis bulk reply
Lua table (array) -> Redis multi bulk reply (truncated to the first nil inside the Lua array if any)
Lua table with a single ok field -> Redis status reply
Lua table with a single err field -> Redis error reply
Lua boolean false -> Redis Nil bulk reply.
будь осторожен:
Числовой тип Lua будет преобразован в целочисленный тип Redis, поэтому, если вы хотите получить десятичное число, вам нужно вернуть число типа String с помощью Lua.