Урок 1.1 установил: последовательность команд не атомарна. Здесь — три инструмента, которые собирают несколько команд в один атомарный шаг: MULTI/EXEC с WATCH, Lua-скрипты и Redis Functions.
MULTI/EXEC: серверная транзакция
MULTI
DECRBY balance:42 100
INCR orders:count
EXEC
1) (integer) 400
2) (integer) 1024
- Команды между MULTI и EXEC ставятся в очередь и исполняются атомарно: никто не вклинится посередине;
- Отката нет: рантайм-ошибка одной команды (WRONGTYPE) не отменяет остальные — Redis транзакции проще SQL-ных;
- Синтаксическая ошибка в очереди отменяет весь EXEC;
- Ошибки исполнения обрабатываются по индексам ответа.
WATCH: оптимистичная блокировка
# Псевдокод: перевод средств без Lua
while true:
WATCH balance:a balance:b
a, b = MGET balance:a balance:b
if a < amount: UNWATCH; return "insufficient"
MULTI
DECRBY balance:a amount
INCRBY balance:b amount
r = EXEC
if r != nil: break # ключи не менялись с WATCH — успех
# иначе: конкурент изменил баланс → повторить цикл
EXEC вернёт nil, если хоть один watched-ключ изменился с момента WATCH. Это check-then-act с автоматической проверкой «никто не вклинился». При высокой конкуренции на один ключ ретраи плодятся — тогда лучше Lua.
Lua: атомарный «суперконман»
Скрипт исполняется целиком в основном потоке — атомарно, и может содержать логику:
-- rate-limit: не более N запросов за окно seconds на ключ
local current = redis.call("INCR", KEYS[1])
if current == 1 then
redis.call("EXPIRE", KEYS[1], ARGV[2])
end
if current > tonumber(ARGV[1]) then
return 0 # отказ
end
return 1 # пропускаем
# EVAL "...скрипт..." 1 rl:user:1001 100 60
Классические применения: check-then-act (лимиты, снятие блокировки — урок 3.4), сравнение и обмен значениями, условные инвалидации, составные операции над хешем.
Правила Lua
- Скрипт блокирует сервер на всё время исполнения — никаких циклов по миллионам элементов, максимум — единицы миллисекунд;
- Детерминированность: в кластере все KEYS должны попасть в один слот; обращение к случайным ключам внутри скрипта — ошибка дизайна;
- Передавайте ключи через
KEYS[], аргументы черезARGV[]— не конкатенируйте ключи в текст скрипта (ломает кластерные инструменты и анализ); - Скрипты кэшируются:
SCRIPT LOAD+EVALSHA sha1 nkeys...экономит передачу тела.
Redis Functions (7+)
FUNCTION LOAD "#!lua name=mylib
redis.register_function('rate_limit', function(keys, args)
-- тело как выше
end)"
# Вызов
FCALL rate_limit 1 rl:user:1001 100 60
Functions — «серверные библиотеки»: зарегистрированы по имени, версионируются, переживают рестарт (сохраняются в датасете). EVAL остаётся для ad-hoc скриптов.
Что выбрать
| Задача | Инструмент |
|---|---|
| Пачка команд без логики | Пайплайн (урок 5.2) |
| Пачка + атомарность, без условий | MULTI/EXEC |
| Check-then-act с ветвлением | Lua / Functions (или WATCH при низкой конкуренции) |
| Высокая конкуренция на ключ | Lua — без ретраев WATCH |
Практика
- Реализуйте перевод средств тремя способами: отдельные команды (покажите гонку), MULTI/EXEC+WATCH, Lua. Нагрузите 20 конкурентами — сравните корректность и производительность.
- Соберите rate-limiter из урока: скользящее окно на ZSet (ZADD+ZREMRANGEBYSCORE+ZCARD в Lua) и фиксированное окно; сравните точность.
- Напишите Function «атомарный перенос элемента из одного ZSet в другой с проверкой membership».
Мини-квиз
1. Есть ли rollback в MULTI/EXEC? — Нет: только атомарность исполнения, не транзакционная целостность.
2. Почему долгий Lua-скрипт опасен? — Блокирует единственный поток исполнения: весь сервер.
3. Почему ключи передают через KEYS? — Кластерный анализ и корректный роутинг; ключи, сконкатенированные в тексте, кластер не видит.