Числа, счётчики и атомарный инкремент
Считать просмотры страницы — задача, которая в обычной БД полна гонок данных. В Redis она решается одной командой.
«Прочитал, прибавил один, записал обратно» — классический рецепт состояния гонки. Redis делает это атомарно, и проблема исчезает.
Допустим, вы считаете просмотры статьи. В реляционной БД наивный подход — прочитать текущее значение, прибавить единицу, записать обратно. Если два запроса делают это одновременно, один инкремент потеряется. В Redis есть атомарные команды, которые делают всё за один шаг.
Команды для чисел
127.0.0.1:6379> SET views 0
OK
127.0.0.1:6379> INCR views
(integer) 1
127.0.0.1:6379> INCR views
(integer) 2
127.0.0.1:6379> INCRBY views 10
(integer) 12
127.0.0.1:6379> DECR views
(integer) 11
127.0.0.1:6379> INCRBYFLOAT price 1.5
"1.5"
INCR увеличивает на 1, DECR уменьшает, INCRBY/DECRBY — на заданное число, INCRBYFLOAT — для дробных. Если ключа не было, Redis считает его нулём и создаёт.
Почему это атомарно
Redis обрабатывает команды последовательно, по одной. Когда выполняется INCR views, никакая другая команда не может вклиниться между чтением и записью. Поэтому даже под нагрузкой в тысячи параллельных запросов каждый инкремент учитывается. Это и делает Redis идеальным для счётчиков.
Гонка данных в обычной БД vs атомарный INCR Без атомарности: С INCR: Поток A: read 5 Поток A: INCR -> 6 Поток B: read 5 Поток B: INCR -> 7 A: write 6 (последовательно, B: write 6 без потерь) Итог: 6 (потеряли +1!) Итог: 7
Демонстрация: атомарный счётчик на чистом Python
Чтобы прочувствовать, как Redis защищает счётчик от гонок, смоделируем оба сценария на stdlib. Запустите — увидите разницу.
import threading
# Небезопасный счётчик: read-modify-write без блокировки
unsafe = {"v": 0}
def bad():
for _ in range(10000):
tmp = unsafe["v"] # read
tmp = tmp + 1 # modify
unsafe["v"] = tmp # write (тут может вклиниться другой поток)
# Атомарный счётчик: как INCR в Redis (операция неделима)
lock = threading.Lock()
safe = {"v": 0}
def good():
for _ in range(10000):
with lock: # имитируем атомарность Redis
safe["v"] += 1
for fn, label in [(bad, "Небезопасный"), (good, "Атомарный (как INCR)")]:
if label.startswith("Не"):
ts = [threading.Thread(target=fn) for _ in range(4)]
else:
ts = [threading.Thread(target=fn) for _ in range(4)]
for t in ts: t.start()
for t in ts: t.join()
print("Небезопасный счётчик:", unsafe["v"], "(ожидалось 40000)")
print("Атомарный счётчик: ", safe["v"], "(ожидалось 40000)")
print("Вывод: без атомарности часть инкрементов теряется.")
print("Redis INCR гарантирует точный результат всегда.")
Небезопасный счётчик почти наверняка покажет число меньше 40000 — это потерянные инкременты. Redis INCR такого не допускает по определению.
Как работает под капотом
Когда значение «выглядит как число», Redis хранит его не как текст, а как нативное целое (encoding int). INCR тогда — это прямое арифметическое сложение в памяти, без парсинга строки. Если значение не парсится как число, INCR вернёт ошибку. Команды счётчиков ограничены 64-битными знаковыми целыми.
Частые ошибки
- Делать INCR над нечисловой строкой — получите ошибку
value is not an integer. - Использовать INCRBYFLOAT для денег без понимания, что float неточен. Для финансов храните копейки как целое и используйте
INCR. - Забывать TTL на счётчиках лимитов — счётчик без срока жизни будет расти вечно.
Best practices
- Для любых счётчиков (просмотры, лайки, лимиты) используйте
INCR/INCRBY— это атомарно и быстро. - Деньги храните в минимальных единицах (копейки) как целые числа.
- Счётчикам с временным окном задавайте TTL через
SET ... EXилиEXPIRE.
Итог: Атомарные команды INCR/DECR/INCRBY решают проблему гонок при подсчёте. Однопоточная природа Redis гарантирует точность даже под высокой нагрузкой. Числа хранятся эффективно как нативные целые.
Счётчики с временным окном
Самое частое применение атомарных счётчиков на практике — счётчики с TTL, которые сами сбрасываются. На них строятся ограничители частоты и метрики «за период»:
# Счётчик запросов на минуту для пользователя
INCR ratelimit:user:42:minute
EXPIRE ratelimit:user:42:minute 60 # окно живёт 60 секунд
Идея: ключ создаётся при первом запросе и автоматически исчезает через минуту. На следующей минуте отсчёт начнётся с нуля — потому что старого ключа уже нет. Так получается «скользящее окно» почти бесплатно. Подробный разбор такого rate-limiter будет в разделе про блокировки и Lua.
Важная тонкость: INCR и EXPIRE — две команды, и между ними возможен сбой, оставляющий ключ без TTL навсегда. Поэтому в продакшене эту пару часто оборачивают в Lua-скрипт, чтобы выполнить их атомарно. Запомните этот паттерн: «инкремент + условный EXPIRE на первом обращении» — он встречается повсюду, от лимитов API до подсчёта голосований.