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 # → PONGredis-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 100Sorted 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 100Stream
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 mykeyPub/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:*:eventsPub/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 /dataBGSAVE # запустить снимок в фоне
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:abc123Leaderboard
# Добавить / обновить 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-lru | LRU только среди ключей с TTL |
volatile-lfu | LFU только среди ключей с 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 mymasterCluster (горизонтальное масштабирование)
Шардирование по 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 или уменьшить maxmemoryToo 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, ... → суммировать)