В прошлом уроке мы клали в ключи строки: названия книг, готовые куски страниц. Но больше всего в Redis кладут не текст, а числа: сколько раз открыли страницу книги, сколько экземпляров осталось на складе, сколько запросов прислал пользователь за минуту. Для них у Redis есть отдельные команды, и одна из них, INCR, решает задачу, на которой спотыкаются почти все, кто пишет счётчик в первый раз.
Счётчик у входа в музей
У входа в музей стоит смотритель с механическим кликером. Каждый посетитель проходит, смотритель жмёт кнопку, цифра увеличивается на единицу. Смотритель не записывает число на бумажку, не прибавляет единицу в уме и не переписывает результат обратно. Он делает одно движение, и кликер сам хранит число.
Теперь представь, что смотрителей трое, а кликер один на всех. Если каждый будет подходить, смотреть на цифру, считать в уме «было 41, станет 42» и выставлять 42 руками, двое могут одновременно увидеть 41 и оба выставить 42. Один посетитель пропадёт. С кнопкой такой беды нет: каждое нажатие прибавляет единицу к тому, что там есть сейчас.
В Redis есть такая кнопка.
INCR и его родственники
Счётчик просмотров книги хранится в обычном строковом ключе. Команда INCR прибавляет к нему единицу и сразу возвращает новое значение:
127.0.0.1:6379> SET book:1:views 0
OK
127.0.0.1:6379> INCR book:1:views
(integer) 1
127.0.0.1:6379> INCR book:1:views
(integer) 2
127.0.0.1:6379> GET book:1:views
"2"
Обрати внимание на разницу в ответах. INCR отвечает (integer) 2, то есть числом. А GET возвращает "2" в кавычках, то есть строкой. Redis хранит значение как строку, но если строка похожа на целое число, команды для чисел умеют с ней работать.
Шаг можно задать любой, и прибавлять, и вычитать:
127.0.0.1:6379> INCRBY book:1:views 10
(integer) 12
127.0.0.1:6379> DECR book:1:views
(integer) 11
127.0.0.1:6379> DECRBY book:1:views 3
(integer) 8
INCRBY прибавляет заданное число, DECR вычитает единицу, DECRBY вычитает заданное число.
Создавать ключ заранее не обязательно. Если ключа нет, Redis считает, что там лежал ноль:
127.0.0.1:6379> INCR book:2:views
(integer) 1
Приложению не нужно проверять, есть ли счётчик у книги, и заводить его при первом просмотре: оно сразу вызывает INCR.
Почему инкремент атомарный
Вернёмся к трём смотрителям. Если писать счётчик «в лоб», приложение делает три шага: прочитать число через GET, прибавить единицу у себя, записать результат через SET. Когда два сервера приложения делают это одновременно, получается так:
сервер А сервер Б
GET book:1:views → "41"
GET book:1:views → "41"
SET book:1:views 42
SET book:1:views 42
Было два просмотра, а счётчик вырос на один. Такая ошибка называется гонкой: результат зависит от того, как случайно перемешались чужие шаги. На тестах её не видно, а под нагрузкой она теряет данные каждый день.
INCR делает чтение, сложение и запись за одну команду внутри сервера. Redis выполняет команды по одной, в одном потоке, поэтому никакая другая команда не может встать посередине. Такое свойство называют атомарностью. Два одновременных INCR всегда дадут плюс два, сколько бы серверов ни слало команды. Подробнее про однопоточную модель Redis говорили в первом уроке раздела.
урок из курса
Урок из курса «Redis с нуля». Курс покупается один раз и остаётся навсегда: 500 ₽.
Оплата пока не подключена: премиум и курсы открываются промокодом.