Атомарный перевод средств
Предпосылки: Клиенты и соединения, EXEC.
Гонка без транзакции
Каждая отдельная команда Redis атомарна, но последовательность GET → решение → SET — нет. Между чтением и записью сервер успевает обработать команду от другого клиента, и тот перезаписывает значение на основе уже устаревших данных:
REDIS.with do |r|
balance = r.get("balance:user:42").to_i # → 100
# ... другой Puma-воркер тоже читает 100
r.set("balance:user:42", balance - 20) # → 80
# другой воркер пишет 100 - 30 = 70
# Итог: 70 вместо 50. Списание 20 потеряно.
endWATCH: оптимистичная блокировка
WATCH позволяет выполнить паттерн «прочитать — решить — записать» безопасно. Redis наблюдает за ключами; если между WATCH и EXEC кто-то изменил наблюдаемый ключ, транзакция отменяется:
def transfer(from_id, to_id, amount)
key_from = "balance:user:#{from_id}"
key_to = "balance:user:#{to_id}"
REDIS.with do |r|
# WATCH помечает ключи для наблюдения:
# если кто-то изменит их до EXEC — транзакция отменится
r.watch(key_from, key_to) do |conn|
balance = conn.get(key_from).to_i
if balance < amount
conn.unwatch # снимаем наблюдение, транзакции не будет
return :insufficient_funds
end
# MULTI открывает транзакцию — команды буферизуются, не выполняются
result = conn.multi do |tx|
tx.decrby(key_from, amount)
tx.incrby(key_to, amount)
end
# EXEC выполняет буфер атомарно; nil — конфликт (WATCH сработал)
result ? :ok : :conflict
end
end
endПри конфликте result будет nil. В продакшен-коде оборачивают retry-циклом:
MAX_RETRIES = 5
MAX_RETRIES.times do
case transfer(42, 99, 20)
when :ok then break
when :conflict then next
when :insufficient_funds then raise "Not enough funds"
end
endПри низкой конкуренции конфликты редки. При высокой конкуренции за одни и те же ключи количество retry растёт — для условной логики без retry лучше подходит Lua-скрипт.
Pipelining внутри MULTI
Библиотека redis-rb при использовании блочной формы r.multi { |tx| ... } автоматически буферизует все команды и отправляет MULTI + команды + EXEC одним пакетом. Транзакция из N команд обходится в один round-trip — pipelining получается бесплатно.