Lua-скрипты

Предпосылки: EXEC, event loop.

EXEC | RDB

Зачем Lua, если есть MULTI/EXEC

EXEC даёт атомарность, но не даёт условной логики: нельзя выполнить «прочитать X, если X > 0 — уменьшить, иначе — вернуть ошибку». Все команды в очереди определены заранее, до выполнения. Lua-скрипт снимает это ограничение: он выполняется атомарно внутри event loop и может содержать произвольную логику — условия, циклы, работу с результатами промежуточных команд.

EVAL: выполнение скрипта

Lua-скрипт передаётся Redis через команду EVAL вместе с ключами и аргументами.

EVAL "return redis.call('GET', KEYS[1])" 1 mykey
--    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^  ^  ^^^^^
--    Lua-код                              |  ключи
--                                    кол-во ключей

KEYS[] — массив ключей, переданных скрипту. ARGV[] — массив дополнительных аргументов. Разделение на KEYS и ARGV — не просто конвенция: в Redis Cluster Redis использует KEYS для определения, на какой узел отправить скрипт. Все ключи, с которыми работает скрипт, должны быть перечислены в KEYS.

Пример: безопасное освобождение блокировки

Классическая задача: удалить ключ только если его значение совпадает с ожидаемым (чтобы не удалить чужую блокировку).

EVAL "
  if redis.call('GET', KEYS[1]) == ARGV[1] then
    return redis.call('DEL', KEYS[1])
  else
    return 0
  end
" 1 lock:order:42 "owner-abc-123"

Скрипт атомарен: между GET и DEL ничего не может произойти. Без Lua пришлось бы делать два отдельных вызова (GET, затем DEL), между которыми другой клиент мог бы изменить значение.

Lua-скрипт блокирует event loop

Атомарность Lua-скриптов — следствие однопоточной модели, но она же создаёт риск. Скрипт выполняется в том же потоке, что и обычные команды. Пока скрипт работает, все остальные клиенты ждут — так же как при любой блокирующей команде (event loop). Параметр lua-time-limit (по умолчанию 5 секунд) не прерывает скрипт — он задаёт порог, после которого Redis начинает отвечать ошибкой BUSY на все входящие команды. Скрипт продолжает выполняться. Администратор может вызвать SCRIPT KILL (для скриптов, не выполнивших запись) или SHUTDOWN NOSAVE.

Следствие: Lua-скрипты должны быть короткими. Тяжёлые вычисления или обход большого количества ключей в скрипте блокируют весь сервер.

EVALSHA и кеширование скриптов

Передача полного тела скрипта при каждом вызове расходует сетевой трафик. При первом вызове EVAL Redis вычисляет хеш скрипта (SHA1 — короткий уникальный идентификатор, вычисленный из текста скрипта) и сохраняет скрипт в кеше. Последующие вызовы можно делать через EVALSHA sha1 numkeys ..., передавая только хеш вместо полного тела.

Команда SCRIPT LOAD загружает скрипт в кеш без выполнения — полезно для предзагрузки при старте приложения. Кеш скриптов хранится только в памяти: при перезапуске Redis он очищается, и скрипты нужно загружать заново. Если EVALSHA вызывается с неизвестным хешем, Redis возвращает ошибку NOSCRIPT — клиент должен повторить вызов через EVAL с полным телом скрипта.

Redis Functions (Redis 7.0+)

EVALSHA экономит трафик, но кеш скриптов не реплицируется и не сохраняется при перезапуске. Redis Functions — развитие Lua-скриптов. Функции регистрируются в Redis с именем и хранятся как часть данных (реплицируются, сохраняются при перезапуске). Вызов — по имени: FCALL function_name numkeys key1 .... Функции группируются в библиотеки и могут использовать общие вспомогательные модули.

-- регистрация библиотеки с функцией:
FUNCTION LOAD "#!lua name=mylib
  redis.register_function('my_unlock', function(keys, args)
    if redis.call('GET', keys[1]) == args[1] then
      return redis.call('DEL', keys[1])
    else
      return 0
    end
  end)
"
 
-- вызов:
FCALL my_unlock 1 lock:order:42 "owner-abc-123"

Преимущество перед EVAL: скрипт загружается один раз, вызывается по имени, не нужно управлять SHA1 хешами. Функции автоматически реплицируются на реплики и восстанавливаются из AOF/RDB.

Когда Lua, когда MULTI/EXEC

Три механизма атомарности в Redis — одна команда, EXEC и Lua — покрывают разные случаи. EXEC — для пакета команд без логики: атомарно увеличить несколько счётчиков, атомарно записать несколько ключей. Lua — для любой логики «если/то»: проверить-и-удалить, проверить-и-обновить, условный инкремент. Общее правило: если между командами нет зависимости по данным, хватает EXEC; если результат одной команды влияет на следующую — нужен Lua.

Sources


EXEC | RDB