В прошлом уроке мы научили Redis переживать перезапуск. Теперь другая беда. В магазине три копии сервиса заказов, и каждая раз в минуту проверяет неоплаченные заказы. Если две копии одновременно возьмутся за заказ 15, покупатель получит два письма «напоминаем об оплате», а склад дважды снимет резерв. Нужно, чтобы за одно дело в каждый момент брался только один процесс. Сегодня строим такую защиту на Redis и знакомимся со скриптами Lua.
Ключ от переговорки
В офисе одна переговорка и один ключ на вахте. Кто взял ключ, тот и занимает комнату. Остальные видят пустой крючок и ждут. Чтобы ключ не пропал навсегда, если человек унёс его домой, вахтёр через час сам забирает его обратно.
И одна важная деталь. Когда человек возвращает ключ, вахтёр смотрит, его ли это ключ. Иначе может случиться так: ты засиделся дольше часа, вахтёр забрал ключ и отдал коллеге, а ты пришёл и «вернул» уже чужую бронь.
Ключ на крючке это распределённая блокировка, а вахта это Redis. «Распределённая» значит, что процессы живут на разных серверах и договариваются через общее хранилище.
Взять блокировку: SET NX PX
Помнишь SET ... NX из урока про строки? Он записывает ключ, только если ключа ещё нет. Это и есть «взять ключ с крючка, если он там есть». Добавляем срок жизни в миллисекундах, PX 30000, это вахтёр, который заберёт ключ через 30 секунд:
127.0.0.1:6379> SET lock:order:15 w1-7f3a NX PX 30000
OK
127.0.0.1:6379> SET lock:order:15 w2-91bc NX PX 30000
(nil)
127.0.0.1:6379> GET lock:order:15
"w1-7f3a"
Первый процесс получил OK и работает с заказом. Второй получил (nil): блокировка занята, заказ 15 он пропускает. Всё в одной команде: проверить и записать отдельными GET и SET нельзя, между ними успеет вклиниться другой процесс. Мы обсуждали эту ловушку в уроке про счётчики.
Значение w1-7f3a это токен: случайная строка, которую процесс придумывает при каждом захвате. Зачем она, видно на снятии блокировки.
Срок жизни обязателен. Без него процесс, который упал посреди работы, оставит блокировку навсегда, и заказ 15 больше никто не обработает.
Снять блокировку: почему не DEL
Самое простое снятие это DEL lock:order:15. Посмотри, что может пойти не так:
- Процесс
w1взял блокировку на 30 секунд. w1завис: долгий запрос в базу, пауза сборщика мусора. Прошло 30 секунд.- Redis удалил ключ по сроку. Процесс
w2взял блокировку со своим токеномw2-91bc. w1проснулся, закончил работу и сделалDEL lock:order:15.- Удалена блокировка
w2. Теперь процессw3тоже может её взять, и с заказом работают двое.
Это шаг 4 из истории с переговоркой: вернул чужой ключ. Поэтому снимать надо так: «удали ключ, только если в нём мой токен». Двумя командами нельзя:
GET lock:order:15 # вижу свой токен
# ...здесь срок истекает, и блокировку берёт w2
DEL lock:order:15 # удаляю чужую
Между GET и DEL есть щель. Нужна одна команда, которая сравнит и удалит за раз. Такой встроенной команды в Redis 7 нет, зато в Redis можно отправить свою маленькую программу. В свежих версиях Redis 8 появилась DELEX lock:book:1 IFEQ токен, которая делает ровно это, но Redis 7 и Valkey ещё долго будут вокруг, а приём со скриптом работает везде.
урок из курса
Урок из курса «Redis с нуля». Курс покупается один раз и остаётся навсегда: 500 ₽.
Оплата пока не подключена: премиум и курсы открываются промокодом.