Redis и кэширование
Сделать медленное быстрым — и сохранить корректность, когда данные меняются.
За 30 секунд
Redis — очень быстрая база данных, которая хранит данные в памяти, а не на диске. Команды используют её, чтобы запоминать ответы на медленные вопросы (кэширование), хранить сессии, считать запросы для rate limit и передавать задачи и сообщения между процессами. Самое сложное здесь не скорость, а решение, сколько могут жить закэшированные данные и как выбрасывать их, когда истина меняется.
Почему я это использую
- Я использую Redis, потому что он убирает самую дорогую работу с горячих путей: один и тот же запрос меню, прайс-листа или настроек не должен ходить в базу данных тысячи раз в минуту.
- Я использую его как основу для фоновой работы и real-time функций — очереди для задач и pub/sub, чтобы каждый экземпляр сервера мог отправить одно и то же событие подключённым клиентам.
- Я использую его для счётчиков и короткоживущего состояния (rate limit, попытки ввода OTP, блокировки), потому что атомарные операции в памяти проще и безопаснее, чем то же самое на строках SQL.
- Я в первую очередь отношусь к кэшированию как к задаче корректности: быстрый неверный ответ хуже медленного верного, поэтому у каждого кэша, который я добавляю, есть понятный владелец, TTL и правило инвалидации.
Как это работает
01Что такое Redis на самом деле
Представьте Redis как огромный общий словарь, который живёт в оперативной памяти. Чтение из памяти занимает микросекунды, а типичный запрос к базе данных — миллисекунды, поэтому для простых выборок Redis часто в 100 раз быстрее. Но Redis — это не только ключ → строка. Он предлагает структуры данных: строки (и счётчики), хэши (объект с полями), списки (очереди), множества (уникальные элементы), сортированные множества (лидерборды, элементы по времени), стримы (лог только на добавление с группами потребителей), а также битовые карты, HyperLogLog и гео-индексы. Каждая команда выполняется атомарно в одном основном потоке, поэтому INCR на счётчике безопасен даже при множестве клиентов. Заметка о лицензиях на 2026 год: Redis 8 доступен под AGPLv3 (наряду с его source-available лицензиями), а Valkey — это форк под лицензией BSD от Linux Foundation, который большинство облаков предлагают как полноценную замену.
- String: закэшированный JSON, счётчики, флаги
- Hash: пользовательская сессия или объект настроек
- List / Stream: очереди задач и журналы событий
- Sorted set: рейтинги, запланированные элементы, rate limit со скользящим окном
SET product:42 '{"name":"Latte","price":1500}' EX 300
GET product:42
INCR rate:203.0.113.10:2026-11-05T10:01
HSET session:abc user_id 7 role cashier
EXPIRE session:abc 1800
ZADD leaderboard 980 player:1 1200 player:2
ZREVRANGE leaderboard 0 9 WITHSCORES
TTL product:42
02Паттерны кэширования: cache-aside, write-through, write-behind
Cache-aside (ленивая загрузка) — вариант по умолчанию: приложение сначала спрашивает Redis; при промахе читает базу данных, сохраняет результат с TTL и возвращает его. Кэшируется только то, что реально запрашивают, а если Redis упадёт, приложение продолжит работать, просто медленнее. Write-through означает, что каждая запись одновременно идёт в базу данных и в кэш, поэтому чтения всегда «тёплые» — хорошо для небольших данных, которые часто читают, например настроек. Write-behind сначала пишет в кэш, а в базу данных сбрасывает позже; это быстро, но может терять данные, поэтому я избегаю его для всего, что связано с деньгами. Каким бы ни был паттерн, источником истины остаётся база данных. Кэш — это копия, и вы должны иметь возможность удалить его целиком, ничего не потеряв.
async function getMenu(branchId: string) {
const key = `menu:v3:${branchId}`;
const hit = await redis.get(key);
if (hit) return JSON.parse(hit);
const menu = await db.menu.findForBranch(branchId);
// TTL with jitter so many keys do not expire at the same moment
const ttl = 600 + Math.floor(Math.random() * 60);
await redis.set(key, JSON.stringify(menu), "EX", ttl);
return menu;
}
async function updateMenu(branchId: string, data: MenuInput) {
await db.menu.update(branchId, data);
await redis.del(`menu:v3:${branchId}`); // invalidate after the write commits
}
03TTL, инвалидация и cache stampede
Закэшированные данные исчезают двумя способами: истекают (TTL) или вы удаляете их намеренно (инвалидация). Используйте оба: инвалидируйте при записях, которые вы контролируете, и держите TTL как страховку для записей, о которых забыли. Добавляйте версию в имена ключей (menu:v3:…), чтобы изменение формы данных никогда не приводило к чтению старых блобов. Удаляйте ключ после коммита транзакции в базе данных, а не до, иначе параллельный читатель может заново наполнить кэш устаревшими данными. Cache stampede (thundering herd) случается, когда горячий ключ истекает, и сотни запросов одновременно получают промах и все идут в базу данных. Решения: добавить к TTL случайный разброс (jitter), позволить пересобирать значение только одному запросу с помощью короткой блокировки (SET key NX EX), отдавать устаревшее значение, пока один воркер его обновляет, или обновлять популярные ключи заранее в фоне, до истечения.
// One rebuilder per key; others wait briefly or serve stale data
const lock = await redis.set(`lock:${key}`, "1", "NX", "EX", 10);
if (lock === "OK") {
try {
const fresh = await loadFromDb();
await redis.set(key, JSON.stringify(fresh), "EX", 600);
} finally {
await redis.del(`lock:${key}`);
}
}
04Сессии, rate limiting и блокировки
Redis — естественное место для короткоживущего общего состояния. Сессии: храните сессию в хэше с TTL, тогда любой сервер приложения может её прочитать, а выход из аккаунта — это один DEL. Rate limiting: фиксированное окно — это INCR по ключу вида rate:{ip}:{minute} с EXPIRE; просто, но допускает всплески на границах окна. Скользящее окно использует сортированное множество временных меток или token bucket в небольшом Lua-скрипте и даёт более плавные лимиты. Используйте ту же идею, чтобы ограничивать попытки OTP и неудачные входы по телефону или аккаунту, а не только по IP. Блокировки: SET lock:order:42 token NX EX 30 даёт одному воркеру эксклюзивную работу на 30 секунд; освобождайте блокировку, только если токен всё ещё совпадает, иначе можно удалить чужую блокировку. Когда речь о реальных деньгах, последней защитой всё равно остаются ограничение в базе данных или ключ идемпотентности.
-- Fixed-window limit: 100 requests per 60 s, atomic in one call
-- KEYS[1] = rate key, ARGV[1] = limit, ARGV[2] = window seconds
local n = redis.call("INCR", KEYS[1])
if n == 1 then
redis.call("EXPIRE", KEYS[1], ARGV[2])
end
if n > tonumber(ARGV[1]) then
return 0
end
return 1
05Очереди и pub/sub
Списки и стримы делают Redis полноценным брокером задач. Библиотеки вроде BullMQ (Node) и очередей Laravel (PHP) хранят задачи в Redis и дают повторы с backoff, задержки, приоритеты и обработку dead-letter. Веб-запрос остаётся быстрым, потому что отправка письма или синхронизация с ERP происходит в воркере. Pub/sub устроен иначе: сообщение рассылается всем, кто подписан прямо сейчас, и нигде не хранится. Это идеально для веерной рассылки real-time событий по нескольким WebSocket-серверам (Redis-адаптер socket.io делает именно это), но если никто не слушает, сообщение пропадает. Когда нужны гарантии доставки или повторное воспроизведение, используйте Streams с группами потребителей или настоящую очередь. Эмпирическое правило: pub/sub — чтобы «сказать всем сейчас», очереди — чтобы «точно убедиться, что это произойдёт».
06Персистентность, память и вытеснение
Redis хранит данные в RAM, но умеет сохранять их на диск. RDB периодически пишет снимки: компактно и быстро восстанавливается, но теряются записи с момента последнего снимка. AOF (append-only file) логирует каждую запись; с appendfsync everysec вы теряете максимум около секунды. Во многих конфигурациях используют оба. Для чистого кэша персистентность можно выключить. Для очередей и сессий включайте AOF. Всегда задавайте maxmemory и политику вытеснения: allkeys-lru или allkeys-lfu для кэша, noeviction для очереди (вам нужны ошибки, а не молча удалённые задачи). В идеале держите кэш и очередь на разных инстансах или базах с разными политиками. Никогда не открывайте порт 6379 в интернет: привязывайтесь к localhost или приватной сети, требуйте пароль или пользователей ACL и используйте TLS между сетями.
# /etc/redis/redis.conf (cache + small queue on a private host)
bind 127.0.0.1
protected-mode yes
requirepass change_me_long_random
maxmemory 1gb
maxmemory-policy allkeys-lfu
appendonly yes
appendfsync everysec
save 900 1
07Когда не стоит кэшировать
Кэширование добавляет вторую копию истины, а каждая копия может ошибаться. Не кэшируйте, пока не измерили: часто индекс или исправленный N+1 запрос решает проблему без новых движущихся частей. Избегайте кэширования данных, которые должны быть точными в момент чтения, — остатков на checkout, балансов кошельков, статуса платежа, — или кэшируйте их только для отображения и перепроверяйте по базе данных внутри транзакции. Не кэшируйте ответы для конкретного пользователя под общим ключом (классическая утечка данных одного клиента другому). Осторожнее с кэшированием того, что меняется почти при каждом запросе: hit rate будет близок к нулю, а за записи вы всё равно платите. Наконец, если набор данных крошечный, а база данных простаивает, вам может хватить кэша в памяти процесса или HTTP-заголовков кэширования.
Когда применять
- Данные, которые часто читают и редко меняют: меню, каталоги, настройки, переводы, курсы валют.
- Дорогие вычисления или результаты сторонних API, которым допустимо устареть на несколько минут.
- Общее короткоживущее состояние между серверами: сессии, OTP-коды, счётчики rate limit, блокировки.
- Очереди задач и real-time рассылка между несколькими экземплярами приложения или WebSocket-серверами.
Когда не стоит
- Как единственная копия важных данных, таких как заказы, платежи или балансы.
- Когда настоящая причина медлительности — отсутствующий индекс или N+1 запрос.
- Для значений, которые должны быть точными в момент принятия решения, если только вы не перепроверяете их по базе данных.
Альтернативы
- Valkey
Форк Redis под лицензией BSD; полностью совместим и предлагается большинством облачных провайдеров.
- Memcached
Простой многопоточный key-value кэш без структур данных и персистентности.
- Кэш в памяти процесса (LRU map)
Максимально быстрый, но у каждого сервера своя копия, и инвалидация сложнее.
- HTTP / CDN кэширование
Кэширует целые ответы на edge с Cache-Control; отлично для публичных, неперсональных страниц.
Лучшие практики
- Считайте базу данных источником истины; вы должны в любой момент безопасно очистить кэш.
- Называйте ключи с префиксом и версией (app:menu:v3:{id}) и всегда задавайте TTL.
- Инвалидируйте после коммита записи и добавляйте jitter к TTL, чтобы избежать синхронного истечения.
- Защищайте горячие ключи от stampede блокировкой пересборки или stale-while-revalidate.
- Задавайте maxmemory и правильную политику вытеснения; для очередей используйте noeviction.
- Держите Redis закрытым (localhost или VPC), с паролем/ACL, никогда на публичном порту.
- Мониторьте hit rate, память, вытеснения и медленные команды; не используйте KEYS * в продакшене (используйте SCAN).
Частые ошибки
- Кэшировать данные конкретного пользователя под общим ключом и показывать одному пользователю данные другого.
- Удалять кэш до коммита транзакции в базе данных, из-за чего обратно записываются устаревшие данные.
- Использовать pub/sub для задач, которые нельзя терять; сообщения пропадают, когда никто не подписан.
- Работать без maxmemory, так что Redis растёт, пока на сервере не закончится RAM.
- Хранить огромные значения (JSON-блобы по мегабайту), которые блокируют единственный поток при каждом чтении.
Глубже
08Один поток: почему одна медленная команда вредит всем
Redis выполняет команды по одной в главном потоке. Начиная с Redis 6, опциональные I/O-потоки (io-threads) параллельно читают и пишут сетевые сокеты, но сами команды по-прежнему выполняются последовательно. Именно поэтому каждая команда атомарна — и именно поэтому одна O(N)-команда останавливает всех остальных клиентов. KEYS * на десяти миллионах ключей, SMEMBERS на огромном множестве, LRANGE 0 -1 на длинном списке или DEL на большом хэше могут блокировать Redis на сотни миллисекунд, а для приложения это выглядит как падение Redis. Перебирайте ключи через SCAN, HSCAN и SSCAN небольшими порциями. Большие ключи удаляйте через UNLINK — он освобождает память в фоновом потоке. Находите нарушителей заранее: SLOWLOG записывает команды дольше заданного порога, LATENCY DOCTOR объясняет всплески задержки, а redis-cli --bigkeys и --memkeys сэмплируют keyspace, не блокируя его.
# Find big keys and memory hogs (samples with SCAN, safe in production)
redis-cli --bigkeys
redis-cli --memkeys
# Commands slower than slowlog-log-slower-than (microseconds)
redis-cli SLOWLOG GET 10
redis-cli LATENCY DOCTOR
# Memory health and cache efficiency
redis-cli INFO memory | grep -E 'used_memory_human|mem_fragmentation_ratio'
redis-cli INFO stats | grep -E 'keyspace_hits|keyspace_misses|evicted_keys'
# Remove one big key without blocking the main thread
redis-cli UNLINK report:2026:full
09Репликация, Sentinel и Cluster
Реплика копирует primary асинхронно: primary сначала отвечает клиенту, а запись отправляет потом. Если primary падает сразу после подтверждения записи, failover может повысить реплику, которая эту запись так и не получила. Команда WAIT блокирует до тех пор, пока N реплик не подтвердят запись: это сужает окно, но не превращает Redis в строго консистентную базу данных. Sentinel следит за primary и его репликами, голосует при сбое и повышает реплику; клиенты спрашивают у Sentinel, где сейчас primary. Cluster решает другую задачу — данные, которые не помещаются на одну машину: keyspace делится на 16,384 hash slot, распределённых по нескольким primary. Команды с несколькими ключами, транзакции и Lua-скрипты работают, только если все ключи попадают в один slot, иначе вы получите ошибку CROSSSLOT. Это решают hash tag: хэшируется только часть внутри {фигурных скобок}, поэтому связанные ключи оказываются вместе.
# Only "user:42" inside the braces is hashed -> same slot
SET {user:42}:profile '{"name":"Ana"}'
SET {user:42}:cart '{"items":3}'
MGET {user:42}:profile {user:42}:cart
CLUSTER KEYSLOT {user:42}:cart
# Without a shared hash tag, MGET across slots returns CROSSSLOT
10Гонка, которая есть в любой системе cache-aside
У cache-aside есть классическая гонка. Читатель A промахивается мимо кэша и читает старую строку. Писатель B обновляет строку и удаляет ключ. Читатель A, чуть замешкавшись, записывает старое значение обратно в кэш, и оно живёт там до истечения TTL. Удаление после commit делает окно маленьким, но не нулевым. Защиты — от дешёвых к надёжным: короткие TTL для данных, где устаревание критично; повторное удаление чуть позже (delayed double delete) из job в очереди; хранение версии или updated_at вместе со значением в кэше и отказ перезаписывать более новое значение с помощью небольшого Lua-скрипта; или инвалидация, которую запускает сама база данных — через transactional outbox или change data capture, чтобы ни один участок кода не мог о ней забыть. Для очень горячих чтений client-side caching в Redis (CLIENT TRACKING, начиная с Redis 6) позволяет серверу отправлять сообщения об инвалидации в near-кэши на уровне приложения.
11Устройство памяти: eviction, кодировки и TTL для отдельных полей
Eviction приблизительный. Вместо того чтобы отслеживать идеальный порядок LRU, Redis сэмплирует несколько ключей (maxmemory-samples, по умолчанию 5) и вытесняет лучшего кандидата; увеличение выборки приближает поведение к настоящему LRU ценой некоторой нагрузки на CPU. LFU хранит для каждого ключа небольшой логарифмический счётчик обращений, который со временем затухает, поэтому ключ, популярный вчера, со временем становится кандидатом на вытеснение. Небольшие хэши, списки, множества и sorted set хранятся в компактной кодировке listpack, поэтому много маленьких хэшей часто занимают меньше памяти, чем много отдельных строковых ключей. Проверяйте отдельный ключ через MEMORY USAGE и следите за mem_fragmentation_ratio; активная дефрагментация (activedefrag yes) может возвращать фрагментированную память на лету. Начиная с Redis 7.4 поля хэша могут истекать по отдельности с помощью HEXPIRE — это удобно для сессий, где поле OTP должно исчезнуть через 60 секунд, а сама сессия продолжает жить. TTL на уровне ключа всё равно удаляет весь хэш, какими бы ни были TTL полей.
Реальные ситуации
Деплой сделал все ключи кэша холодными
Проблема
В релизе префикс ключей сменился с menu:v3 на menu:v4. За минуту каждый запрос стал промахиваться мимо кэша, CPU базы данных упёрся в 100%, а задержка checkout выросла с 80 ms до нескольких секунд.
Как решить
В краткосрочной перспективе откатитесь на старый префикс или переведите читающий трафик на реплику, чтобы база данных пришла в себя. Затем исправьте процесс. Смену версии ключей нужно прогревать до переключения трафика: job перебирает активные филиалы и заполняет menu:v4, пока старый код ещё читает v3. Добавьте request coalescing (одна пересборка на ключ под SET lock NX EX), чтобы одновременные промахи не умножали нагрузку на базу данных. Добавьте jitter к TTL, чтобы новые ключи потом не истекли одновременно. Наконец, выведите hit rate кэша и число соединений с базой данных на дашборд деплоя с алертом — тогда холодный кэш будет виден за секунды, а не по жалобам клиентов.
Фоновые job пропали из-за нехватки памяти
Проблема
Один инстанс Redis хранил и кэш, и очередь job с maxmemory-policy allkeys-lru. Во время всплеска трафика Redis вытеснил ключи очереди, и часть писем с подтверждением и webhook так и не была отправлена.
Как решить
Разделите нагрузки. Перенесите очередь на отдельный инстанс (или отдельную managed-базу) с maxmemory-policy noeviction и включённым AOF, чтобы переполненная очередь давала ошибки, на которые можно настроить алерт, а не молча теряла job. Кэш оставьте на allkeys-lfu. Определяйте размер обоих по реальным метрикам: used_memory, evicted_keys и глубина очереди. Поскольку письма и webhook потерялись, воспроизведите их из источника истины: выберите заказы за затронутый период, у которых статус уведомления всё ещё pending, и снова поставьте их в очередь с ключами идемпотентности, чтобы клиенты, которые уже получили сообщение, не получили его дважды.
Клиенты видели старые цены после обновления
Проблема
Администраторы изменили цены, API удалил ключ кэша после commit, но несколько филиалов показывали старую цену ещё десять минут. Это происходило только под нагрузкой и не воспроизводилось локально.
Как решить
Это гонка cache-aside: медленный читатель загрузил старую строку до обновления и записал её обратно после удаления. Подтвердите это, логируя записи в кэш вместе с updated_at строки. Затем сделайте записи условными: храните значение вместе с версией и записывайте его через короткий Lua-скрипт, который отказывается перезаписывать более новую версию. Добавьте отложенное повторное удаление из job в очереди через несколько секунд после обновления как дешёвую страховку и сократите TTL для данных о ценах. При checkout всё равно читайте цены из базы данных внутри транзакции — тогда устаревший кэш может повлиять только на отображение, но никогда на списание.
Шпаргалка
- Перебрать ключи без блокировки
SCAN 0 MATCH menu:v3:* COUNT 200- Удалить большой ключ в фоне
UNLINK key- Блокировка с токеном владельца и таймаутом
SET lock:order:42 <token> NX EX 30- TTL для одного поля хэша (Redis 7.4+)
HEXPIRE session:abc 60 FIELDS 1 otp- Проверить оставшееся время жизни
TTL key (-1 = no TTL, -2 = missing)- Память, занятая одним ключом
MEMORY USAGE key- Безопасно найти большие ключи
redis-cli --bigkeys- Посмотреть медленные команды
SLOWLOG GET 10- Hit rate кэша
INFO stats -> keyspace_hits / (keyspace_hits + keyspace_misses)- В какой slot кластера попадает ключ
CLUSTER KEYSLOT {user:42}:cart
В моих реальных проектах
- В мультитенантной POS SaaS для ресторанов Redis pub/sub рассылает real-time события по комнатам socket.io для каждой компании, филиала, терминала и пользователя.
- На той же платформе работает около 20 очередей BullMQ на Redis, которые наполняются через transactional outbox, включая вебхуки с HMAC-подписью, доставляемые через очередь.
- На выделенном сервере этого продукта Redis работает в Docker Compose рядом с PostgreSQL, воркерами BullMQ и стеком наблюдаемости из Prometheus, Grafana и Loki.
Вопросы и ответы
Q1В чём разница между cache-aside и write-through?
В cache-aside логикой владеет приложение: читать из кэша, а при промахе загрузить из базы данных, сохранить значение с TTL и вернуть его. Записи идут в базу данных, а затем инвалидируют ключ. Кэшируется только то, что используется, и система переживает падение кэша. В write-through каждая запись обновляет базу данных и кэш вместе, поэтому чтения всегда «тёплые» — ценой записи данных, которые, возможно, никто не прочитает. По умолчанию я использую cache-aside с явной инвалидацией плюс TTL, а write-through — для небольших и очень горячих данных конфигурации.
Q2Как предотвратить cache stampede?
Stampede случается, когда популярный ключ истекает и множество запросов одновременно пересобирают его, перегружая базу данных. Я сочетаю несколько защит. TTL получают случайный jitter, чтобы ключи не истекали одновременно. Пересборка защищена короткой блокировкой через SET NX EX, поэтому пересчитывает один запрос, а остальные недолго ждут или получают предыдущее значение. Для самых горячих ключей я использую stale-while-revalidate: продолжаю отдавать старое значение и обновляю его в фоне до истечения. Со стороны базы данных разумные лимиты пула соединений не дают одной плохой минуте превратиться в аварию.
Q3Redis хранит данные в памяти. Что будет, если он перезапустится?
Зависит от настроек персистентности. Со снимками RDB вы теряете записи с момента последнего снимка; с AOF и appendfsync everysec — максимум около секунды; с выключенной персистентностью — всё. Для чистого кэша это допустимо, потому что база данных — источник истины, а кэш снова прогреется, хотя во время прогрева стоит ожидать дополнительной нагрузки. Для очередей и сессий я включаю AOF, а для критичных нагрузок использую реплику с автоматическим failover через Sentinel или managed-сервис. Важные бизнес-данные никогда не живут только в Redis.
Q4Как бы вы реализовали rate limiting на Redis?
Самый простой вариант — фиксированное окно: INCR по ключу вида rate:{client}:{minute} и установка EXPIRE при первом создании счётчика, всё в одном Lua-скрипте, чтобы операция была атомарной. Если счётчик превышает лимит, возвращается HTTP 429 с заголовком Retry-After. Фиксированные окна допускают всплески на границах, поэтому для более строгих лимитов я использую скользящее окно с сортированным множеством временных меток или token bucket. Я ограничиваю по правильному идентификатору — IP, API-ключ, аккаунт или номер телефона для OTP — и применяю более жёсткие лимиты к чувствительным эндпоинтам вроде входа и платежей.
Q5Когда вы выберете Redis pub/sub, а когда очередь?
Pub/sub рассылает сообщение каждому подписчику, подключённому в данный момент, и ничего не хранит. Это подходит для real-time рассылки, например отправки события о статусе заказа на каждый WebSocket-сервер, чтобы его получил нужный клиент. Очередь хранит задачу, пока воркер её не обработает, с повторами, backoff и отслеживанием сбоев. Это подходит для работы, которая должна произойти ровно так, как просили: письма, вебхуки, синхронизация с ERP, чеки. Если мне нужно повторное воспроизведение или несколько групп потребителей, читающих один лог, я использую Redis Streams или специализированный брокер вроде Kafka.
Q6Какую политику вытеснения вы ставите и почему?
Для кэша я задаю maxmemory и обычно allkeys-lfu или allkeys-lru, чтобы при заполнении памяти Redis удалял наименее полезные ключи, а не отказывал в записи. LFU сохраняет часто используемые элементы, даже если к ним не обращались в последнюю секунду. Для очередей, сессий и блокировок я использую noeviction, потому что молча удалённая задача или сессия хуже ошибки, на которую можно поставить алерт. Поскольку у одного инстанса одна политика, я предпочитаю отдельные инстансы или логическое разделение для кэша и очередей и мониторю метрику evicted_keys.
Вопросы уровня senior
S1Достаточно ли блокировки в Redis, чтобы защитить платёж или списание остатка?
Сама по себе — нет. Блокировка SET NX EX — хорошая блокировка для эффективности: в большинстве случаев она не даёт двум воркерам выполнять одну и ту же дорогую работу. Но это не гарантия корректности. Процесс может зависнуть — garbage collection, медленная сеть, заморозка VM — дольше срока блокировки, пока другой воркер берёт блокировку, и в итоге пишут оба. Failover тоже может потерять блокировку, которая не успела реплицироваться. Redlock на нескольких инстансах снимает часть сценариев сбоя, но всё равно опирается на предположения о времени. Для корректности последний рубеж должен быть в системе учёта: unique constraint, ключ идемпотентности, условное обновление вроде UPDATE stock SET qty = qty - 1 WHERE qty > 0 или fencing token, который проверяет база данных. Блокировки Redis я использую, чтобы избежать дублирующей работы, а инвариант пусть обеспечивает база данных.
S2Sentinel, Cluster или managed-сервис — как выбрать?
Начинайте с задачи. Если данные спокойно помещаются в память одной машины и нужна высокая доступность, проще primary с репликами и Sentinel или их managed-аналог: работают все команды, у Lua-скриптов и транзакций нет правил про slot, клиенты остаются простыми. Cluster выбирайте, когда нужно больше памяти или пропускной способности на запись, чем даёт один узел; цена — проектирование hash slot, ограничения CROSSSLOT, клиенты с поддержкой кластера и более сложная эксплуатация, например решардинг. Managed-сервисы снимают с вас патчи, бэкапы и учения по failover — ценой более высокой стоимости и меньшего контроля над версиями и модулями. Для большинства продуктовых бэкендов, которые я видел, один правильно подобранный по размеру primary с persistence и репликой позволяет зайти очень далеко; шардинг — решение, которое диктуют измерения, а не мода.
S3Как спроектировать кэширование для multi-tenant SaaS?
Три аспекта: изоляция, справедливость и инвалидация. Изоляция: ID тенанта всегда входит в ключ, который строит одна вспомогательная функция, а не конкатенация строк в случайных местах; данные конкретного пользователя включают ID пользователя. Отсутствующий префикс тенанта — это утечка данных, поэтому я тестирую построители ключей напрямую. Справедливость: один крупный тенант может заполнить память и вытеснить ключи всех остальных, поэтому я слежу за памятью по префиксам ключей, ограничиваю размер кэшируемых значений и для очень крупных тенантов рассматриваю выделенные инстансы. Инвалидация: изменения на уровне всего тенанта, например новое меню или прайс-лист, должны увеличивать номер версии тенанта, который хранится в Redis и встроен в ключи, — тогда один INCR инвалидирует всё для этого тенанта без сканирования и удаления тысяч ключей, а старые записи просто истекают по TTL.
S4Когда стоит заменить pub/sub на Redis Streams и что при этом меняется?
Когда потеря сообщения становится недопустимой или потребителям нужно догонять пропущенное. Pub/sub работает по принципу fire-and-forget: подписчик, переподключившийся после деплоя, пропускает всё, что было опубликовано за это время. Streams хранят записи в append-only логе; consumer groups отслеживают, что прочитал каждый потребитель, записи остаются в статусе pending до подтверждения через XACK, а XAUTOCLAIM позволяет другому потребителю забрать сообщения, которые упавший воркер так и не обработал. Издержки реальны: поток нужно обрезать (MAXLEN или MINID), иначе память растёт бесконечно; обработчики должны быть идемпотентными, потому что повторная доставка случается; нужно мониторить списки pending-записей. Для live-рассылки в UI через несколько WebSocket-серверов я по-прежнему использую pub/sub, потому что пропущенное событие исправляется следующим запросом. Для всего, что запускает бизнес-действия, мне нужны Streams, очередь job или отдельный брокер.
Глоссарий
- TTL
- Time to live: сколько секунд ключ существует, прежде чем Redis автоматически его удалит.
- Hit ratio
- Доля чтений, обслуженных из кэша, а не из базы данных; главная мера полезности кэша.
- Eviction
- Вытеснение ключей Redis, чтобы оставаться в пределах maxmemory; ключи выбираются по настроенной политике, например LRU или LFU.
- Cache stampede
- Множество запросов одновременно промахиваются по одному истёкшему ключу и все пересобирают его из базы данных.
- Hot key
- Один ключ, который читают или пишут так часто, что нагрузка концентрируется на одном потоке или одном узле кластера.
- Big key
- Ключ с очень большим значением или коллекцией, из-за чего команды над ним медленные и блокирующие.
- Hash slot
- Одна из 16,384 корзин в Redis Cluster; каждый ключ попадает в один slot, каждый slot — на один узел.
- Hash tag
- Часть имени ключа в {скобках}, которая одна хэшируется и заставляет связанные ключи попасть в один slot кластера.
- Sentinel
- Группа процессов Redis, которая следит за primary, обнаруживает сбой и автоматически повышает реплику.
- Fencing token
- Возрастающий номер, выдаваемый вместе с блокировкой, чтобы хранилище могло отклонять записи владельца с истёкшей блокировкой.
Что изучить дальше
Есть идея продукта?Давайте создадим его.
Расскажите о своей идее, продукте или команде. Обычно отвечаю в течение одного рабочего дня.