Новая поза! Вызов скриптов Lua в Redis для выполнения атомарных операций

Redis

Справочная информация: есть Лидер поставщика услуг и несколько Рабочих подписчиков на сообщения. Лидер — это программа управления очередью, которая поддерживает очередь пользователей.Когда ресурс простаивает и назначается пользователю в очереди, Лидер отправляет сообщение подписчику (сообщение имеет уникальный идентификатор).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.

Выполняем содержимое скриптаreturn {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.