Раздел 37 · Redis с нуля

Транзакции: MULTI, EXEC, WATCH

junior~25 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

В прошлом уроке мы разобрались с надёжной обработкой событий. Теперь вернёмся к задаче из урока 6: продать последнюю книгу так, чтобы склад не ушёл в минус, даже если покупатели жмут «купить» одновременно. Там мы познакомились с MULTI, EXEC и WATCH на коротких примерах. Сегодня разбираем, что именно они обещают, где ловушки и как написать покупку с повтором в настоящем коде.

Три обещания и одно «нет»

Коротко вспомним урок 6. MULTI открывает очередь, команды отвечают QUEUED, EXEC выполняет их все подряд. Из этого следуют три обещания Redis:

  1. Никто не влезет между командами. Redis однопоточный, и очередь из EXEC выполняется одним куском. Другие клиенты ждут.
  2. Синтаксическая ошибка отменяет всё. Если команда в очереди написана неправильно, EXEC ответит EXECABORT и не выполнит ничего.
  3. WATCH отменяет транзакцию, если ключ изменился. Тогда EXEC вернёт (nil).

И одно «нет»: отката нет. Если команда упала уже при выполнении, остальные всё равно выполнятся. Вот пример, которого не было в уроке 6: ключ tags:book:1 это множество, а мы по ошибке делаем ему INCR:

127.0.0.1:6379> SADD tags:book:1 classic
(integer) 1
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> INCR tags:book:1
QUEUED
127.0.0.1:6379(TX)> SADD tags:book:1 novel
QUEUED
127.0.0.1:6379(TX)> EXEC
1) (error) WRONGTYPE Operation against a key holding the wrong kind of value
2) (integer) 1

При постановке в очередь Redis проверяет только имя команды и число аргументов. Что лежит в ключе, он узнаёт при выполнении. Первая команда упала, вторая добавила тег. Транзакция Redis обещает «подряд и без чужих», а не «всё или ничего».

Сравни с PostgreSQL из урока про ACID: там упавшая команда переводит транзакцию в ошибку, и COMMIT превращается в откат. В Redis ошибки в данных ловят до транзакции, в коде приложения.

Синтаксические ошибки Redis ловит при постановке в очередь, и тогда отменяет всё:

127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> INCRBY stock:book:3
(error) ERR wrong number of arguments for 'incrby' command
127.0.0.1:6379(TX)> EXEC
(error) EXECABORT Transaction discarded because of previous errors.

С неизвестной командой, например опечаткой INCRR, будет то же самое.

Внутри MULTI ответа нет

Главное ограничение транзакции легко пропустить. Внутри MULTI команды не выполняются, поэтому их ответы недоступны:

127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> GET stock:book:3
QUEUED
127.0.0.1:6379(TX)> DECR stock:book:3
QUEUED
127.0.0.1:6379(TX)> EXEC
1) "1"
2) (integer) 0

Остаток "1" пришёл только в ответе EXEC, когда DECR уже выполнился. Написать «если остаток больше нуля, то уменьши» внутри MULTI нельзя: решение принимается в приложении, а приложение узнаёт остаток слишком поздно.

Значит, читать надо до MULTI. Но между чтением и EXEC другой покупатель может забрать книгу. Эту щель и закрывает WATCH.

Тетрадь с карандашной отметкой

Представь общую тетрадь заказов в книжном. Продавец хочет продать последний экземпляр. Он смотрит в тетрадь, видит «остаток 1» и ставит на полях карандашную отметку: «я смотрел сюда». Потом идёт за книгой на склад. Вернувшись, он проверяет: если кто-то за это время писал на этой странице, отметка стёрта, и он начинает заново. Если отметка на месте, он спокойно записывает продажу.

Продавец ничего не запирал, другие могли писать в тетрадь сколько угодно. Он просто проверил в конце, не помешали ли ему. Это и есть оптимистичная блокировка. Её противоположность, пессимистичная, запирает данные на время работы, как SELECT ... FOR UPDATE в уроке про блокировки PostgreSQL.

урок из курса

Урок из курса «Redis с нуля». Курс покупается один раз и остаётся навсегда: 500 ₽.

Оплата пока не подключена: премиум и курсы открываются промокодом.