Транзакции и пайплайнинг

Иногда нужно выполнить несколько команд как единое целое или отправить их пачкой. Redis даёт для этого два разных механизма.

Транзакция отвечает на вопрос «выполнить всё вместе?». Пайплайнинг отвечает на вопрос «отправить всё разом по сети?». Это не одно и то же.

Два механизма, которые часто путают: транзакции (MULTI/EXEC) и пайплайнинг. Первый — про атомарность группы команд. Второй — про производительность сети. Разберём оба.

Транзакции: MULTI / EXEC

127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET account:a 100
QUEUED
127.0.0.1:6379> INCRBY account:b 50
QUEUED
127.0.0.1:6379> EXEC
1) OK
2) (integer) 50

MULTI начинает транзакцию. Команды не выполняются сразу — они ставятся в очередь (QUEUED). EXEC выполняет их все подряд, атомарно: между ними не вклинится команда другого клиента. DISCARD отменяет транзакцию.

Важно понимать: транзакции Redis — это не транзакции SQL. Здесь нет отката (rollback) при ошибке выполнения одной команды. Если команда синтаксически корректна, но падает в рантайме (например, INCR над строкой), остальные всё равно выполнятся.

Оптимистичная блокировка: WATCH

WATCH следит за ключом. Если ключ изменился до EXEC, транзакция отменяется. Это «оптимистичная блокировка» — основа для безопасного read-modify-write:

WATCH balance
val = GET balance        # читаем
MULTI
SET balance <новое>       # если balance не менялся — применится
EXEC                     # иначе вернёт nil, повторяем

Пайплайнинг: меньше сетевых кругов

Пайплайнинг — это отправка нескольких команд одним пакетом, не дожидаясь ответа на каждую. Это резко снижает влияние сетевой задержки:

   Без пайплайна:           С пайплайном:
   send CMD1                send CMD1
   wait ответ               send CMD2     все разом
   send CMD2                send CMD3
   wait ответ               recv 3 ответа
   send CMD3
   wait ответ               1 round-trip вместо 3
   = 3 round-trip

Если у вас 100 команд и пинг до сервера 1 мс, без пайплайна это 100 мс ожидания. С пайплайном — около 1 мс. Разница колоссальна.

Как работает под капотом

При MULTI Redis копит команды в очереди клиента, проверяя только синтаксис. На EXEC вся очередь выполняется как один непрерывный блок — другие клиенты ждут. WATCH реализует механизм CAS (compare-and-swap): сервер запоминает «версию» наблюдаемых ключей, и если она изменилась, EXEC возвращает nil. Пайплайнинг же — чисто клиентская оптимизация транспорта: сервер просто получает несколько команд в буфере и отвечает на все.

Частые ошибки

  • Ждать от MULTI/EXEC отката как в SQL. Его нет. Проверяйте корректность данных заранее.
  • Путать транзакцию и пайплайн. Пайплайн не делает команды атомарными — он лишь группирует их по сети.
  • Сложная условная логика в транзакции. MULTI/EXEC не умеет ветвиться по промежуточным результатам — для этого нужны Lua-скрипты.

Best practices

  • Для атомарной группы команд — MULTI/EXEC, при необходимости с WATCH.
  • Для массовых операций — пайплайнинг, чтобы убрать сетевые задержки.
  • Когда нужна логика «прочитать, решить, записать» атомарно — используйте Lua-скрипты (об этом в разделе про лимиты).

Итог: MULTI/EXEC группирует команды атомарно, но без SQL-отката. WATCH даёт оптимистичную блокировку через CAS. Пайплайнинг — это про сеть, а не про атомарность. Для сложной условной логики используйте Lua.

Когда нужен Lua вместо транзакции

Граница между MULTI/EXEC и Lua-скриптами часто вызывает путаницу, поэтому зафиксируем её чётко. Транзакция умеет только выполнить заранее известный набор команд атомарно. Она не может посмотреть на промежуточный результат и решить, что делать дальше.

# Это НЕЛЬЗЯ выразить транзакцией — нужна ветка по результату:
# "прочитать счётчик; если меньше лимита — увеличить и разрешить,
#  иначе отказать"

# В Lua это естественно:
EVAL "local c = redis.call('INCR', KEYS[1])
      if c == 1 then redis.call('EXPIRE', KEYS[1], ARGV[1]) end
      if c > tonumber(ARGV[2]) then return 0 end
      return 1" 1 limit:user42 60 100

Скрипт читает счётчик, ставит TTL на первом обращении и сравнивает с лимитом — всё это атомарно, как одна команда. Транзакция так не умеет, потому что не ветвится по данным. Поэтому правило: нужна простая группировка нескольких команд — берите MULTI/EXEC; нужна логика «прочитал, решил, записал» — берите Lua.

И помните про третий, ортогональный инструмент — пайплайнинг. Его можно комбинировать с любым из двух: отправить пачкой хоть набор обычных команд, хоть несколько транзакций, экономя сетевые круги.

Проверьте себя
1. Чем транзакция Redis (MULTI/EXEC) отличается от транзакции SQL?
AВ Redis транзакции работают быстрее, но в остальном идентичны
BВ Redis нет отката (rollback): если команда падает в рантайме, остальные всё равно выполняются
CВ Redis транзакция может содержать только одну команду
DВ Redis транзакции не атомарны вообще
2. Что даёт пайплайнинг в Redis?
AДелает группу команд атомарной
BСнижает влияние сетевой задержки, отправляя команды пачкой без ожидания каждого ответа
CШифрует трафик между клиентом и сервером
DАвтоматически повторяет упавшие команды