В прошлом уроке мы разобрались с надёжной обработкой событий. Теперь вернёмся к задаче из урока 6: продать последнюю книгу так, чтобы склад не ушёл в минус, даже если покупатели жмут «купить» одновременно. Там мы познакомились с MULTI, EXEC и WATCH на коротких примерах. Сегодня разбираем, что именно они обещают, где ловушки и как написать покупку с повтором в настоящем коде.
Три обещания и одно «нет»
Коротко вспомним урок 6. MULTI открывает очередь, команды отвечают QUEUED, EXEC выполняет их все подряд. Из этого следуют три обещания Redis:
- Никто не влезет между командами. Redis однопоточный, и очередь из
EXECвыполняется одним куском. Другие клиенты ждут. - Синтаксическая ошибка отменяет всё. Если команда в очереди написана неправильно,
EXECответитEXECABORTи не выполнит ничего. 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 ₽.
Оплата пока не подключена: премиум и курсы открываются промокодом.