Два самых частых способа разгрузить сервис: кеш (не считать и не читать повторно то, что уже есть) и очередь (не делать в ответе пользователю то, что можно сделать потом). Оба приносят свои проблемы: устаревшие данные и повторную обработку.
Где кешировать
- В памяти процесса (
Map,lru-cache): самый быстрый, но у каждого экземпляра свой, пропадает при рестарте, растёт вместе с памятью процесса. Ограничивай размер. - Redis: общий для всех экземпляров, данные в памяти, доступ за доли миллисекунды по сети. Стандартный выбор.
- HTTP-кеш и CDN: для публичных ответов, через
Cache-Control.
Стратегии
Cache-aside (ленивое кеширование), самая частая:
async function getProduct(id) {
const key = `product:${id}`;
const cached = await redis.get(key);
if (cached) return JSON.parse(cached);
const product = await db.product.findUnique({ where: { id } });
await redis.set(key, JSON.stringify(product), 'EX', 300); // TTL 5 минут
return product;
}
При изменении товара ключ удаляют, а не обновляют: так меньше шансов записать в кеш устаревшее значение при гонке двух запросов.
- Write-through: при записи обновляем и базу, и кеш сразу.
- Write-behind: пишем в кеш, в базу позже пачкой. Быстро, но при падении можно потерять данные.
TTL нужен почти всегда: даже если инвалидация где-то забыта, данные устареют не навсегда.
Проблемы кеша
- Инвалидация: при изменении данных нужно найти и сбросить все ключи, где они лежат. Чем больше производных ключей (списки, агрегаты), тем сложнее.
- Cache stampede (лавина): популярный ключ истёк, и тысяча запросов одновременно идёт в базу. Лечится блокировкой на пересчёт (
SET key NX EX), объединением одинаковых запросов в один промис, ранним фоновым обновлением. - Одновременное истечение: тысячи ключей с одинаковым TTL истекают в одну секунду. Добавляй к TTL случайный разброс (jitter).
- Память Redis: когда она кончается, поведение задаёт
maxmemory-policy. По умолчанию этоnoeviction: записи начинают отклоняться с ошибкой. Для кеша обычно ставятallkeys-lruилиallkeys-lfu.
Redis: что нужно знать
Команды Redis выполняются по одной (основной поток обрабатывает их последовательно), поэтому каждая команда атомарна. INCR безопасен для счётчиков и rate limit. SET key value NX EX 30 атомарно ставит ключ, только если его нет, это основа простой распределённой блокировки. Долгие команды вроде KEYS * блокируют весь Redis, вместо них SCAN.
Очереди
Очередь отделяет приём задачи от её выполнения. Пользователь получил ответ «заказ принят», а письмо, PDF и интеграции делаются фоном воркерами.
- BullMQ: очереди задач поверх Redis, популярны в Node. Повторы, задержки, приоритеты, ограничение параллелизма.
- RabbitMQ: брокер сообщений с маршрутизацией и подтверждениями.
- Kafka: распределённый журнал событий, хранит сообщения и позволяет перечитывать, высокая пропускная способность.
Надёжность обработки
Почти все очереди дают доставку at-least-once: если воркер упал до подтверждения, задача придёт снова. Значит, обработчик должен быть идемпотентным: повтор не создаёт второй платёж или второе письмо. Приёмы: уникальный ключ операции в базе, проверка «уже сделано», upsert.
Повторы делают с экспоненциальной задержкой и разбросом (1 с, 2 с, 4 с...), чтобы не добивать упавший сервис. Задачи, исчерпавшие попытки, уходят в dead letter queue для разбора.
Transactional outbox: если нужно и записать в базу, и отправить событие, запись события кладут в таблицу outbox в той же транзакции, а отдельный процесс публикует её в брокер. Иначе возможна запись без события или событие без записи.
Частые ошибки
- Кеш без TTL и без лимита размера.
- Обновлять кеш вместо удаления при изменении.
- Неидемпотентные обработчики задач.
- Бесконечные мгновенные повторы.
KEYS *в проде.
Что спросят на собесе
- Какие стратегии кеширования знаешь и как инвалидировать кеш.
- Что такое cache stampede и как его избежать.
- Зачем очередь и что значит at-least-once.
- Как сделать обработчик идемпотентным.
- Чем Redis-очередь отличается от Kafka.