Программа курса

Модуль 05 / Урок 22 из 27

Транзакции, Lua-скрипты и Functions

Теория и практика

Урок 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

Практика

  1. Реализуйте перевод средств тремя способами: отдельные команды (покажите гонку), MULTI/EXEC+WATCH, Lua. Нагрузите 20 конкурентами — сравните корректность и производительность.
  2. Соберите rate-limiter из урока: скользящее окно на ZSet (ZADD+ZREMRANGEBYSCORE+ZCARD в Lua) и фиксированное окно; сравните точность.
  3. Напишите Function «атомарный перенос элемента из одного ZSet в другой с проверкой membership».
Мини-квиз

1. Есть ли rollback в MULTI/EXEC? — Нет: только атомарность исполнения, не транзакционная целостность.

2. Почему долгий Lua-скрипт опасен? — Блокирует единственный поток исполнения: весь сервер.

3. Почему ключи передают через KEYS? — Кластерный анализ и корректный роутинг; ключи, сконкатенированные в тексте, кластер не видит.