Redis: in-memory хранилище

Концепции

Client (redis-cli, приложение)
  → Redis Server (однопоточная event loop)
    → Database (0-15, по умолчанию 0)
      → Key → Value (string, hash, list, set, sorted set, stream, ...)

In-memory: все данные в оперативной памяти. Чтение/запись за O(1), то есть микросекунды. Данные можно персистить на диск (RDB, AOF), но основной storage остаётся RAM.

Однопоточность: команды выполняются последовательно, одна за другой. Нет race conditions, нет блокировок. Одна команда является атомарной операцией. Пропускная способность: сотни тысяч операций в секунду.

Key-Value: всё хранится как ключ → значение. Ключ всегда является строкой. Значение представляет собой одну из структур данных (string, hash, list, set, sorted set, stream, …).

TTL: время жизни ключа. По истечении происходит автоматическое удаление. Основной механизм для кешей и сессий.

Pub/Sub: публикация/подписка на каналы. Для real-time уведомлений.

Lua scripting: атомарное выполнение нескольких команд на сервере.

Типичные use-cases:
─────────────────────────────────────────────
Кеш                  — результаты запросов, HTML-фрагменты, API-ответы
Сессии                — данные пользовательских сессий (вместо cookie)
Rate limiting         — ограничение количества запросов
Очереди               — задачи для воркеров (list или stream)
Pub/Sub               — real-time уведомления, чат, events
Leaderboard           — рейтинги, топы (sorted set)
Счётчики              — просмотры, лайки, онлайн-пользователи
Distributed lock      — блокировки в распределённой системе
Геолокация            — поиск ближайших объектов
Bloom filter          — вероятностная проверка "видели ли раньше"

Установка

# Arch
sudo pacman -S redis
sudo systemctl enable --now redis

# Ubuntu
sudo apt install redis-server
sudo systemctl enable --now redis-server

# macOS
brew install redis
brew services start redis

# Docker
docker run -d --name redis \
  -p 6379:6379 \
  redis:7-alpine \
  redis-server --requirepass secret --maxmemory 256mb --maxmemory-policy allkeys-lru

# Docker Compose
# compose.yaml
services:
  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    command: redis-server --requirepass secret --maxmemory 256mb --maxmemory-policy allkeys-lru
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "-a", "secret", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5

volumes:
  redis_data:

Подключение

redis-cli                                # localhost:6379
redis-cli -h host -p 6379 -a password
redis-cli -u redis://user:pass@host:6379/0
redis-cli --tls -h host -p 6380         # TLS

# Из Docker
docker exec -it redis redis-cli -a secret

# Проверка
redis-cli PING                           # → PONG

redis-cli

redis-cli                                # интерактивный режим
redis-cli GET mykey                      # одноразовая команда
redis-cli -n 2 GET mykey                 # база данных №2
redis-cli --scan --pattern "user:*"      # перебрать ключи по паттерну
redis-cli --bigkeys                      # найти самые большие ключи
redis-cli --memkeys                      # потребление памяти по ключам
redis-cli --stat                         # мониторинг в реальном времени
redis-cli --latency                      # измерить задержку
redis-cli --pipe < commands.txt          # массовое выполнение (pipeline)
redis-cli MONITOR                        # логировать все команды в реальном времени

Общие команды

SELECT 2                                 # переключиться на базу 2
DBSIZE                                   # количество ключей в текущей базе
FLUSHDB                                  # удалить все ключи в текущей базе
FLUSHALL                                 # удалить все ключи во всех базах
INFO                                     # полная информация о сервере
INFO memory                              # только секция памяти
INFO stats                               # статистика операций
INFO keyspace                            # статистика по базам
CONFIG GET maxmemory                     # получить настройку
CONFIG SET maxmemory 512mb               # изменить настройку на лету
SLOWLOG GET 10                           # последние 10 медленных команд
CLIENT LIST                              # подключённые клиенты

Структуры данных

String

Базовый тип. Хранит строку, число или бинарные данные (до 512 MB).

# Строки
SET name "Alice"                         # установить
GET name                                 # получить → "Alice"
GETSET name "Bob"                        # получить старое и установить новое
SETNX name "Carol"                       # установить только если не существует (atomic)
MSET k1 "v1" k2 "v2" k3 "v3"            # установить несколько
MGET k1 k2 k3                           # получить несколько → ["v1", "v2", "v3"]
APPEND name " Smith"                     # дописать → "Bob Smith"
STRLEN name                              # длина → 9
GETRANGE name 0 2                        # подстрока → "Bob"
SETRANGE name 0 "Rob"                    # заменить с позиции → "Rob Smith"

# Числа (строки, которые Redis интерпретирует как числа)
SET counter 10
INCR counter                             # +1 → 11 (атомарно)
INCRBY counter 5                         # +5 → 16
DECR counter                             # -1 → 15
DECRBY counter 3                         # -3 → 12
INCRBYFLOAT price 0.50                   # +0.50 (float)

# TTL — время жизни
SET session "data" EX 3600               # истекает через 3600 секунд
SET session "data" PX 60000              # через 60000 миллисекунд
SET token "abc" EXAT 1735689600          # в конкретный unix timestamp
SETEX session 3600 "data"                # SET + EX (старый синтаксис)
PSETEX session 60000 "data"              # SET + PX

# SET с опциями (Redis 6.2+)
SET lock "owner1" NX EX 30               # SET если не существует + TTL 30s (distributed lock)
SET key "val" XX                         # SET только если уже существует
SET key "val" GET                        # SET и вернуть предыдущее значение

# TTL-команды (для любого типа)
EXPIRE key 60                            # установить TTL 60 секунд
PEXPIRE key 60000                        # TTL в миллисекундах
EXPIREAT key 1735689600                  # TTL до конкретного timestamp
TTL key                                  # оставшееся время (-1 = нет TTL, -2 = не существует)
PTTL key                                 # TTL в миллисекундах
PERSIST key                              # убрать TTL (сделать вечным)

Hash

Словарь (поле → значение) внутри одного ключа. Идеален для объектов.

# Установить поля
HSET user:1 name "Alice" email "alice@example.com" role "admin"
HSETNX user:1 phone "+7900"              # только если поле не существует

# Получить
HGET user:1 name                         # → "Alice"
HMGET user:1 name email role             # несколько полей → ["Alice", "alice@...", "admin"]
HGETALL user:1                           # все поля и значения → [name, Alice, email, ...]

# Проверки
HEXISTS user:1 phone                     # поле существует? → 0/1
HLEN user:1                              # количество полей → 4
HKEYS user:1                             # все ключи → [name, email, role, phone]
HVALS user:1                             # все значения → [Alice, alice@..., admin, +7900]

# Удалить поле
HDEL user:1 phone

# Числовые поля
HSET product:1 views 0 price 29.99
HINCRBY product:1 views 1                # +1 → 1 (атомарно)
HINCRBYFLOAT product:1 price -5.00       # -5.00 → 24.99

# Итерация (курсор, safe для больших хешей)
HSCAN user:1 0 MATCH "e*" COUNT 100
Паттерн: Hash вместо множества String-ов

ПЛОХО (3 ключа на пользователя):
SET user:1:name "Alice"
SET user:1:email "alice@example.com"
SET user:1:role "admin"

ХОРОШО (1 ключ = 1 объект):
HSET user:1 name "Alice" email "alice@example.com" role "admin"

Hash с малым числом полей хранится в ziplist — компактнее и быстрее.

List

Двусвязный список. Быстрая вставка/удаление с обоих концов. Для очередей, стеков, последних N элементов.

# Добавить
LPUSH queue "task3" "task2" "task1"      # в начало (слева) → [task1, task2, task3]
RPUSH queue "task4"                      # в конец (справа) → [task1, task2, task3, task4]

# Получить
LRANGE queue 0 -1                        # весь список → [task1, task2, task3, task4]
LRANGE queue 0 2                         # первые 3 → [task1, task2, task3]
LINDEX queue 0                           # по индексу → "task1"
LLEN queue                               # длина → 4

# Извлечь (удаляет элемент)
LPOP queue                               # из начала → "task1"
RPOP queue                               # из конца → "task4"
LPOP queue 2                             # несколько из начала → ["task2", "task3"]

# Очередь с блокировкой (worker ждёт новых задач)
BLPOP queue 30                           # блокировать до 30 секунд
BRPOP queue 0                            # блокировать бесконечно

# Перемещение между списками (атомарно)
LMOVE source dest LEFT RIGHT             # взять из source слева, положить в dest справа
BLMOVE source dest LEFT RIGHT 30         # блокирующий вариант

# Обрезать (оставить только [start, stop])
LTRIM notifications 0 99                 # оставить первые 100 элементов

# Удалить по значению
LREM queue 1 "task2"                     # удалить 1 вхождение "task2"
LREM queue 0 "task2"                     # удалить все вхождения

# Установить по индексу
LSET queue 0 "new_task"
Паттерны:
─────────────────────────────────────
Очередь (FIFO):     RPUSH (добавить) + LPOP (взять)
Стек (LIFO):        LPUSH (добавить) + LPOP (взять)
Capped list:        LPUSH + LTRIM 0 99 (последние 100 элементов)
Worker queue:       RPUSH + BLPOP (блокирующий потребитель)
Reliable queue:     RPUSH + LMOVE (перемещение в processing-список)

Set

Неупорядоченное множество уникальных строк. Для тегов, ролей, проверки членства, пересечений.

# Добавить
SADD tags "redis" "cache" "nosql"
SADD online:users "user:1" "user:2" "user:3"

# Получить
SMEMBERS tags                            # все элементы → [redis, cache, nosql]
SCARD tags                               # количество → 3
SISMEMBER tags "redis"                   # входит ли? → 1 (true)
SMISMEMBER tags "redis" "sql" "cache"    # несколько проверок → [1, 0, 1]
SRANDMEMBER tags 2                       # 2 случайных (без удаления)
SPOP tags                                # случайный + удалить

# Удалить
SREM tags "nosql"                        # удалить элемент

# Операции над множествами
SADD set1 "a" "b" "c"
SADD set2 "b" "c" "d"

SINTER set1 set2                         # пересечение → [b, c]
SUNION set1 set2                         # объединение → [a, b, c, d]
SDIFF set1 set2                          # разность (в set1, но не в set2) → [a]

# Сохранить результат
SINTERSTORE result set1 set2             # пересечение → result
SUNIONSTORE result set1 set2             # объединение → result

# Итерация
SSCAN tags 0 MATCH "re*" COUNT 100

Sorted Set (ZSet)

Множество с числовым score для каждого элемента. Автоматическая сортировка по score. Для рейтингов, приоритетных очередей, time series.

# Добавить (score, member)
ZADD leaderboard 1500 "alice" 1200 "bob" 1800 "carol"

# Добавить с опциями
ZADD leaderboard NX 1000 "dave"          # только если не существует
ZADD leaderboard XX 1600 "alice"         # только если существует (обновить)
ZADD leaderboard GT 1400 "alice"         # обновить только если новый score больше
ZADD leaderboard LT 1400 "alice"         # обновить только если новый score меньше
ZINCRBY leaderboard 50 "alice"           # увеличить score на 50

# По рангу (позиции)
ZRANGE leaderboard 0 -1                  # все, от низшего score к высшему
ZRANGE leaderboard 0 -1 WITHSCORES      # с score-ами
ZRANGE leaderboard 0 2                   # топ-3 (по возрастанию)
ZREVRANGE leaderboard 0 2                # топ-3 (по убыванию) — для рейтинга
ZRANGE leaderboard 0 9 REV WITHSCORES   # топ-10 (Redis 6.2+ синтаксис)
ZRANK leaderboard "alice"                # позиция (0-based, от низшего)
ZREVRANK leaderboard "alice"             # позиция (от высшего) — место в рейтинге

# По score
ZRANGEBYSCORE leaderboard 1000 1500      # score от 1000 до 1500
ZRANGEBYSCORE leaderboard -inf +inf      # все
ZRANGEBYSCORE leaderboard (1000 1500     # score > 1000 (исключая)
ZCOUNT leaderboard 1000 1500             # количество в диапазоне

# Информация
ZSCORE leaderboard "alice"               # score элемента
ZMSCORE leaderboard "alice" "bob"        # несколько score-ов
ZCARD leaderboard                        # количество элементов

# Удалить
ZREM leaderboard "dave"
ZREMRANGEBYSCORE leaderboard -inf 1000   # удалить все с score <= 1000
ZREMRANGEBYRANK leaderboard 0 2          # удалить первые 3 (с наименьшим score)

# Операции над множествами
ZINTERSTORE result 2 zset1 zset2 WEIGHTS 1 2    # пересечение (score = s1*1 + s2*2)
ZUNIONSTORE result 2 zset1 zset2 AGGREGATE MIN   # объединение (score = min)

# Итерация
ZSCAN leaderboard 0 MATCH "a*" COUNT 100

Stream

Append-only лог с consumer groups. Для очередей событий, event sourcing, message broker.

# Добавить запись (* = автогенерация ID)
XADD events * type "purchase" user_id "42" amount "99.99"
XADD events * type "pageview" user_id "42" page "/home"
# ID формат: <timestamp_ms>-<sequence> → "1710500000000-0"

# Явный ID
XADD events 1710500000000-0 type "custom" data "value"

# Максимальная длина стрима
XADD events MAXLEN ~ 1000000 * type "event" data "value"  # ~приблизительно 1M записей
XADD events MINID ~ 1710000000000-0 * type "event"        # удалить старше ID

# Чтение
XLEN events                              # количество записей
XRANGE events - +                        # все записи (от первой до последней)
XRANGE events - + COUNT 10               # первые 10
XRANGE events 1710500000000-0 +          # от конкретного ID
XREVRANGE events + - COUNT 10            # последние 10

# Блокирующее чтение (ожидание новых записей)
XREAD COUNT 10 BLOCK 5000 STREAMS events $
# $ = только новые записи, BLOCK 5000 = ждать 5 секунд

# Чтение нескольких стримов
XREAD COUNT 10 BLOCK 0 STREAMS events notifications $ $

# --- Consumer Groups ---

# Создать группу потребителей
XGROUP CREATE events mygroup $ MKSTREAM
# $ = читать только новые, 0 = читать с начала

# Читать как потребитель в группе
XREADGROUP GROUP mygroup consumer1 COUNT 10 BLOCK 5000 STREAMS events >
# > = только новые (не прочитанные группой) записи

# Подтвердить обработку
XACK events mygroup 1710500000000-0 1710500000001-0

# Необработанные записи (pending)
XPENDING events mygroup - + 10           # список pending записей
XPENDING events mygroup IDLE 60000 - + 10  # зависшие > 60 секунд

# Забрать зависшие записи у другого потребителя
XCLAIM events mygroup consumer2 60000 1710500000000-0

# Автоматический claim (проще)
XAUTOCLAIM events mygroup consumer2 60000 0-0 COUNT 10

# Информация
XINFO STREAM events                      # информация о стриме
XINFO GROUPS events                      # группы потребителей
XINFO CONSUMERS events mygroup           # потребители в группе

# Удалить
XDEL events 1710500000000-0             # удалить запись
XTRIM events MAXLEN 1000000              # обрезать
XGROUP DESTROY events mygroup            # удалить группу
Consumer Group гарантирует:
1. Каждое сообщение доставляется ровно одному consumer-у в группе
2. Необработанные сообщения можно перехватить (XCLAIM) при сбое consumer-а
3. XACK подтверждает обработку (как в RabbitMQ/Kafka)

HyperLogLog

Вероятностная структура для подсчёта уникальных элементов. Фиксированно 12 KB памяти, ~0.81% погрешность. Для unique visitors, unique events.

PFADD visitors "user:1" "user:2" "user:3"
PFADD visitors "user:2" "user:4"         # дубликат user:2 не считается
PFCOUNT visitors                         # ~4

# Объединение (за все дни)
PFADD visitors:day1 "u1" "u2" "u3"
PFADD visitors:day2 "u2" "u3" "u4"
PFMERGE visitors:week visitors:day1 visitors:day2
PFCOUNT visitors:week                    # ~4 (уникальные за оба дня)

Bitmap

Строка, интерпретируемая как массив бит. Компактный способ хранить boolean-флаги.

# Установить бит
SETBIT active_users:2025-03-15 42 1      # пользователь 42 был активен
SETBIT active_users:2025-03-15 100 1     # пользователь 100 был активен

# Проверить
GETBIT active_users:2025-03-15 42        # → 1

# Посчитать единицы (активных пользователей)
BITCOUNT active_users:2025-03-15         # → 2

# Побитовые операции (пересечение дней = пользователи активные оба дня)
BITOP AND active_both active_users:2025-03-15 active_users:2025-03-16
BITOP OR active_any active_users:2025-03-15 active_users:2025-03-16
BITCOUNT active_both                     # пользователи, активные оба дня

Geospatial

# Добавить координаты
GEOADD locations 37.6173 55.7558 "Moscow"
GEOADD locations 30.3158 59.9398 "SPb" 49.1060 55.7960 "Kazan"

# Расстояние
GEODIST locations "Moscow" "SPb" km      # ~634 км

# Поиск рядом
GEOSEARCH locations FROMLONLAT 37.6 55.7 BYRADIUS 100 km ASC COUNT 5 WITHCOORD WITHDIST
GEOSEARCH locations FROMMEMBER "Moscow" BYRADIUS 800 km ASC WITHCOORD WITHDIST

# Координаты
GEOPOS locations "Moscow"                # → [37.617..., 55.755...]
GEOHASH locations "Moscow"               # → geohash строка

Ключи: управление

# Поиск ключей
KEYS user:*                              # найти по паттерну (БЛОКИРУЕТ! не для prod)
SCAN 0 MATCH user:* COUNT 100           # итеративный поиск (safe для prod)
# SCAN возвращает [cursor, keys]. Повторять с новым cursor, пока cursor != 0

# Информация
EXISTS key                               # существует? → 0/1
EXISTS k1 k2 k3                          # сколько из них существуют → 0-3
TYPE key                                 # тип → string/hash/list/set/zset/stream
OBJECT ENCODING key                      # внутреннее кодирование → ziplist/hashtable/...
OBJECT IDLETIME key                      # секунды с последнего доступа
MEMORY USAGE key                         # потребление памяти в байтах
DEBUG OBJECT key                         # подробная информация

# TTL
EXPIRE key 60                            # TTL 60 секунд
PEXPIRE key 60000                        # TTL в миллисекундах
EXPIREAT key 1735689600                  # до unix timestamp
TTL key                                  # оставшееся время (-1 нет TTL, -2 не существует)
PERSIST key                              # убрать TTL

# Переименование
RENAME key newkey
RENAMENX key newkey                      # только если newkey не существует

# Удаление
DEL key                                  # синхронное удаление
UNLINK key                               # асинхронное (неблокирующее) удаление
DEL k1 k2 k3                            # несколько ключей

# Копирование
COPY source dest                         # скопировать значение
COPY source dest REPLACE                 # перезаписать если dest существует

# Сериализация
DUMP key                                 # сериализовать значение
RESTORE key 0 "\x00\x..."               # восстановить (0 = без TTL)

KEYS: никогда в production. Блокирует сервер на время сканирования. Используй SCAN.

Паттерны именования ключей

object:id:field            — user:42:name
object:id                  — session:abc123
object:field:value         — user:email:alice@example.com (обратный индекс)
env:object:id              — prod:cache:query:7f3a2b

Разделитель : является конвенцией. Используй осмысленные префиксы, чтобы было легко группировать и находить ключи.

Транзакции и атомарность

MULTI/EXEC

# Группа команд, выполняемых атомарно
MULTI                                    # начать транзакцию
SET balance:1 900
SET balance:2 1100
EXEC                                     # выполнить всё

# Отмена
MULTI
SET key "value"
DISCARD                                  # отменить всё

# Optimistic locking с WATCH
WATCH balance:1                          # следить за ключом
val = GET balance:1                      # прочитать
MULTI
SET balance:1 (val - 100)                # изменить
EXEC                                     # если balance:1 изменился между WATCH и EXEC → nil (откат)
# Если EXEC вернул nil — повторить попытку

MULTI/EXEC работает НЕ как в SQL. Нет ROLLBACK при ошибке одной команды. Все команды выполнятся, ошибочные вернут ошибку. Это гарантия атомарности выполнения, а не транзакционной изоляции.

Lua-скрипты

Полностью атомарное выполнение на сервере. Замена транзакций для сложной логики:

# Выполнить Lua-скрипт
EVAL "return redis.call('GET', KEYS[1])" 1 mykey

# Rate limiter (атомарный)
EVAL "
local current = tonumber(redis.call('GET', KEYS[1]) or '0')
if current >= tonumber(ARGV[1]) then
    return 0
end
redis.call('INCR', KEYS[1])
if current == 0 then
    redis.call('EXPIRE', KEYS[1], ARGV[2])
end
return 1
" 1 ratelimit:user:42 100 60
# KEYS[1] = ratelimit:user:42, ARGV[1] = limit (100), ARGV[2] = window (60s)

# Загрузить скрипт (вернёт SHA)
SCRIPT LOAD "return redis.call('GET', KEYS[1])"
# → "a42059b356c875f0717db19a51f6aaa9161571a2"

# Вызвать по SHA (быстрее — не парсить каждый раз)
EVALSHA "a42059b356c875f0717db19a51f6aaa9161571a2" 1 mykey

# Redis Functions (Redis 7+, замена EVAL)
FUNCTION LOAD "#!lua name=mylib
redis.register_function('my_get', function(keys, args)
    return redis.call('GET', keys[1])
end)
"
FCALL my_get 1 mykey

Pub/Sub

# Подписаться на канал (блокирующее)
SUBSCRIBE notifications
SUBSCRIBE channel1 channel2

# Подписка по паттерну
PSUBSCRIBE user:*:events                 # все каналы user:XXX:events

# Опубликовать (из другого клиента)
PUBLISH notifications '{"type": "new_order", "id": 123}'
PUBLISH user:42:events '{"type": "login"}'

# Отписаться
UNSUBSCRIBE notifications
PUNSUBSCRIBE user:*:events

Pub/Sub работает по принципу fire-and-forget. Если подписчика нет, сообщение теряется. Для гарантированной доставки используй Streams.

Persistence (сохранение на диск)

RDB (снимки)

redis.conf:
save 3600 1          # снимок каждый час, если >= 1 изменение
save 300 100         # каждые 5 минут, если >= 100 изменений
save 60 10000        # каждую минуту, если >= 10000 изменений
dbfilename dump.rdb
dir /data
BGSAVE                                   # запустить снимок в фоне
LASTSAVE                                 # timestamp последнего снимка

+ Компактный файл, быстрое восстановление. - Потеря данных между снимками (до N минут).

AOF (журнал операций)

redis.conf:
appendonly yes
appendfilename "appendonly.aof"
appendfsync everysec                     # fsync каждую секунду (баланс)
# appendfsync always                     # fsync на каждую запись (медленно, надёжно)
# appendfsync no                         # OS решает когда fsync (быстро, ненадёжно)
BGREWRITEAOF                             # переписать AOF (сжать)

+ Минимальная потеря данных (до 1 секунды). - Файл больше RDB, восстановление медленнее.

Рекомендация

Включай оба: RDB для быстрого восстановления + AOF для минимальной потери данных. Redis при старте использует AOF (если включён), иначе RDB.

Паттерны

Кеш

# Cache-aside (основной паттерн)
# 1. Проверить кеш
GET cache:user:42
# 2. Если miss → запросить БД → записать в кеш
SET cache:user:42 '{"name":"Alice",...}' EX 300

# Кеш с автообновлением TTL при чтении
GET cache:query:abc
EXPIRE cache:query:abc 300               # продлить TTL при каждом чтении

Distributed Lock

# Захват (атомарный SET NX с TTL)
SET lock:order:123 "worker:1" NX EX 30   # NX = только если не существует
# Вернул OK → блокировка получена
# Вернул nil → занято

# Освобождение (Lua-скрипт — проверить владельца перед удалением)
EVAL "
if redis.call('GET', KEYS[1]) == ARGV[1] then
    return redis.call('DEL', KEYS[1])
else
    return 0
end
" 1 lock:order:123 "worker:1"

Для production distributed lock используй Redlock или библиотеки (redisson, redis-py lock).

Rate Limiter

# Fixed window (простой)
EVAL "
local count = redis.call('INCR', KEYS[1])
if count == 1 then
    redis.call('EXPIRE', KEYS[1], ARGV[1])
end
return count
" 1 ratelimit:api:user:42 60
# Если count > limit → отклонить запрос

# Sliding window (точнее)
EVAL "
local key = KEYS[1]
local now = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local limit = tonumber(ARGV[3])
redis.call('ZREMRANGEBYSCORE', key, 0, now - window)
local count = redis.call('ZCARD', key)
if count < limit then
    redis.call('ZADD', key, now, now .. '-' .. math.random(1000000))
    redis.call('EXPIRE', key, window)
    return 1
end
return 0
" 1 ratelimit:user:42 <timestamp_ms> 60000 100

Сессии

# Создать сессию
HSET session:abc123 user_id 42 role "admin" created_at "2025-03-15T14:30:00Z"
EXPIRE session:abc123 86400              # 24 часа

# Проверить/обновить
HGETALL session:abc123
EXPIRE session:abc123 86400              # продлить при активности

# Удалить (logout)
DEL session:abc123

Leaderboard

# Добавить / обновить score
ZADD leaderboard 1500 "player:alice"
ZINCRBY leaderboard 10 "player:alice"    # +10 очков

# Топ-10
ZREVRANGE leaderboard 0 9 WITHSCORES

# Ранг игрока (место в рейтинге, 0-based)
ZREVRANK leaderboard "player:alice"

# Топ-10 за сегодня (отдельный ключ)
ZADD leaderboard:2025-03-15 1500 "player:alice"
ZREVRANGE leaderboard:2025-03-15 0 9 WITHSCORES
EXPIRE leaderboard:2025-03-15 172800     # TTL 2 дня

Очередь задач (простая)

# Producer
RPUSH jobs '{"type":"send_email","to":"alice@example.com"}'

# Consumer (блокирующий)
BLPOP jobs 0                             # ждать бесконечно

# Reliable queue (с подтверждением)
LMOVE jobs processing LEFT RIGHT         # взять из jobs → положить в processing
# ... обработать ...
LREM processing 1 '<job_data>'           # удалить из processing после успеха
# Если consumer упал — job остаётся в processing → переработать

Для серьёзных очередей используй Streams с Consumer Groups.

Счётчики и аналитика

# Простой счётчик
INCR page:views:/home

# Счётчик по периодам
INCR page:views:/home:2025-03-15
EXPIRE page:views:/home:2025-03-15 604800   # неделя

# Уникальные посетители
PFADD visitors:2025-03-15 "user:42" "user:100"
PFCOUNT visitors:2025-03-15

# Уникальные за неделю
PFMERGE visitors:week visitors:2025-03-15 visitors:2025-03-16 ...
PFCOUNT visitors:week

# Online-пользователи (с автоочисткой)
ZADD online_users <timestamp> "user:42"  # score = время последнего heartbeat
ZRANGEBYSCORE online_users (now-300) +inf  # активные за последние 5 минут
ZREMRANGEBYSCORE online_users -inf (now-300)  # удалить неактивных

Pipeline и производительность

Pipeline

Отправка нескольких команд без ожидания ответа на каждую. Уменьшает RTT (round-trip time):

# redis-cli с pipeline
echo -e "SET k1 v1\nSET k2 v2\nGET k1\nGET k2" | redis-cli --pipe

# В приложении (Python)
pipe = r.pipeline()
pipe.set('k1', 'v1')
pipe.set('k2', 'v2')
pipe.get('k1')
pipe.get('k2')
results = pipe.execute()                 # один round-trip вместо четырёх

Без pipeline: 4 команды × 0.1ms RTT = 0.4ms. С pipeline: 1 round-trip × 0.1ms RTT = 0.1ms (+ время выполнения).

Оптимизация памяти

# Проверить потребление
INFO memory
MEMORY DOCTOR                            # диагностика
MEMORY USAGE key                         # конкретный ключ

# Настройки сжатия (ziplist для малых структур)
# redis.conf:
hash-max-ziplist-entries 128             # Hash в ziplist до 128 полей
hash-max-ziplist-value 64                # Hash в ziplist до 64 байт на значение
list-max-ziplist-size -2                 # List в ziplist до 8KB
set-max-intset-entries 512               # Set из чисел в intset до 512 элементов
zset-max-ziplist-entries 128             # ZSet в ziplist до 128 элементов

Maxmemory и eviction

# redis.conf или CONFIG SET
maxmemory 256mb

# Политика вытеснения при достижении лимита
maxmemory-policy allkeys-lru             # основной для кеша
ПолитикаОписание
noevictionошибка при записи (по умолчанию)
allkeys-lruудалять наименее используемые ключи (основной для кеша)
allkeys-lfuудалять наименее частые (Redis 4.0+)
allkeys-randomудалять случайные
volatile-lruLRU только среди ключей с TTL
volatile-lfuLFU только среди ключей с TTL
volatile-ttlудалять с наименьшим TTL

Latency

redis-cli --latency                      # средняя задержка
redis-cli --latency-history              # история задержки
redis-cli --intrinsic-latency 10         # baseline задержка системы (10 сек тест)

# Slow log
SLOWLOG GET 10                           # последние 10 медленных команд
CONFIG SET slowlog-log-slower-than 10000 # порог в микросекундах (10ms)

Клиенты в приложениях

Python (redis-py)

import redis

r = redis.Redis(host='localhost', port=6379, password='secret', decode_responses=True)

# Базовые операции
r.set('key', 'value', ex=60)
value = r.get('key')

# Hash
r.hset('user:1', mapping={'name': 'Alice', 'email': 'alice@example.com'})
user = r.hgetall('user:1')

# Pipeline
with r.pipeline() as pipe:
    pipe.set('k1', 'v1')
    pipe.set('k2', 'v2')
    pipe.get('k1')
    results = pipe.execute()

# Pub/Sub
pubsub = r.pubsub()
pubsub.subscribe('notifications')
for message in pubsub.listen():
    if message['type'] == 'message':
        print(message['data'])

# Connection pool (автоматический в redis-py)
pool = redis.ConnectionPool(host='localhost', port=6379, max_connections=20)
r = redis.Redis(connection_pool=pool)

Go (go-redis)

import "github.com/redis/go-redis/v9"

rdb := redis.NewClient(&redis.Options{
    Addr:     "localhost:6379",
    Password: "secret",
    DB:       0,
    PoolSize: 20,
})

// Базовые операции
rdb.Set(ctx, "key", "value", 60*time.Second)
val, err := rdb.Get(ctx, "key").Result()

// Hash
rdb.HSet(ctx, "user:1", "name", "Alice", "email", "alice@example.com")
user, _ := rdb.HGetAll(ctx, "user:1").Result()

// Pipeline
pipe := rdb.Pipeline()
pipe.Set(ctx, "k1", "v1", 0)
pipe.Set(ctx, "k2", "v2", 0)
pipe.Get(ctx, "k1")
results, _ := pipe.Exec(ctx)

Node.js (ioredis)

import Redis from 'ioredis';

const redis = new Redis({
  host: 'localhost',
  port: 6379,
  password: 'secret',
  maxRetriesPerRequest: 3,
});

// Базовые операции
await redis.set('key', 'value', 'EX', 60);
const value = await redis.get('key');

// Pipeline
const pipeline = redis.pipeline();
pipeline.set('k1', 'v1');
pipeline.set('k2', 'v2');
pipeline.get('k1');
const results = await pipeline.exec();

// Pub/Sub
const sub = new Redis();
sub.subscribe('notifications');
sub.on('message', (channel, message) => {
  console.log(channel, message);
});

Sentinel и Cluster

Sentinel (высокая доступность)

Автоматический failover: если master упал, один из replica повышается:

redis.conf (master):
  # стандартная конфигурация

redis.conf (replica):
  replicaof master-host 6379

sentinel.conf:
  sentinel monitor mymaster master-host 6379 2    # 2 = quorum
  sentinel down-after-milliseconds mymaster 5000
  sentinel failover-timeout mymaster 10000
# Подключение через Sentinel (клиент автоматически находит master)
redis-cli -p 26379 SENTINEL get-master-addr-by-name mymaster

Cluster (горизонтальное масштабирование)

Шардирование по hash slots (16384 слота):

# Создать кластер
redis-cli --cluster create \
  node1:6379 node2:6379 node3:6379 \
  node4:6379 node5:6379 node6:6379 \
  --cluster-replicas 1

# Информация
redis-cli -c CLUSTER INFO
redis-cli -c CLUSTER NODES

# Подключение к кластеру (флаг -c)
redis-cli -c -h node1 -p 6379

В Cluster-режиме: команды с несколькими ключами работают только если ключи в одном слоте. Используй hash tags: {user:42}:profile и {user:42}:sessions гарантированно в одном слоте.

Частые проблемы

KEYS блокирует сервер:

Никогда не используй KEYS * в production. Сканирует все ключи, блокируя Redis.

# Вместо KEYS — используй SCAN
SCAN 0 MATCH "user:*" COUNT 100

Память закончилась (OOM):

INFO memory                              # проверить used_memory
CONFIG GET maxmemory                     # лимит
CONFIG SET maxmemory-policy allkeys-lru  # включить вытеснение
redis-cli --bigkeys                      # найти самые большие ключи

# Найти ключи без TTL (потенциальная утечка)
SCAN 0 COUNT 1000
# Для каждого ключа: TTL key → если -1, то без TTL

Высокая latency:

redis-cli --latency-history              # мониторинг
SLOWLOG GET 10                           # медленные команды

# Частые причины:
# 1. KEYS, SMEMBERS на огромных множествах → SCAN
# 2. Огромные значения (>10KB) → разбить на части
# 3. BGSAVE на слабом диске → настроить реже или AOF
# 4. swap (Redis ушёл в swap) → увеличить RAM или уменьшить maxmemory

Too many connections:

INFO clients                             # текущие подключения
CONFIG GET maxclients                    # лимит (по умолчанию 10000)
CLIENT LIST                              # список клиентов

# В приложении: используй connection pool
# Не создавай новое подключение на каждый запрос

Данные потерялись после рестарта:

Проверь persistence:

CONFIG GET save                          # RDB включён?
CONFIG GET appendonly                    # AOF включён?

Если оба выключены, данные хранятся только в памяти. Включи хотя бы AOF для важных данных.

Hot key (один ключ перегружает сервер):

redis-cli --hotkeys                      # найти горячие ключи (нужен LFU)
# Решения:
# 1. Локальный кеш в приложении (L1 cache)
# 2. Реплики для чтения
# 3. Разбить ключ на несколько (counter:1, counter:2, ... → суммировать)