Lua-скрипты
Предпосылки: EXEC, event loop.
Зачем 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
- Redis Documentation: Scripting with Lua. https://redis.io/docs/interact/programmability/eval-intro/
- Redis Documentation: Functions. https://redis.io/docs/interact/programmability/functions-intro/
- Redis source:
src/scripting.c,src/function.c