Архитектура кеширования: Redis, Memcached
Архитектура кеширования: Redis, Memcached
1) Когда и зачем кеш
Цели: снизить латентность, разгрузить БД/PSP/внешние API и смягчить пики.
Слои кешей часто многоуровневые: in-process (L1) → service-level (Redis/Memcached L2) → edge/CDN. Внутренний кеш ускоряет «горячие» чтения, L2 — общий для сервисов, edge — для публичного контента.
2) Redis vs Memcached — кратко
Правило: если нужны сложные структуры данных, персистентность, pub/sub/стримы/скрипты — берите Redis. Если супер-простая, быстрая, дешевая KV-кеш-прослойка без durability — Memcached.
3) Шаблоны кеширования
3.1 Cache-aside (lazy)
Приложение читает из кеша → промах → читает из БД → кладет в кеш с TTL.
Простой контроль TTL, независимость от кеша. − Возможен «шторм» при промахах.
3.2 Read-through
Клиент/прокси сам подтягивает из origin при промахе и кладет в кеш.
Централизованная логика. − Сложнее инфраструктурно.
3.3 Write-through / Write-behind
Write-through: запись сначала в кеш, затем в БД.
Write-behind: запись в очередь, асинхронный флаш в БД (потенциальная потеря при краше — нужен журнал).
3.4 Two-tier (L1+L2)
L1 (in-process) c коротким TTL и soft TTL, L2 (Redis/Memcached) — «истина кеша». Инвалидация через pub/sub.
4) TTL, штормы и консистентность
TTL: задавайте близко к частоте изменения данных. Для горячих ключей используйте рандомизацию TTL (jitter): `ttl = base ± rand(0..base0.1)` — снимает синхронные истечения.
Dogpile (thundering herd): защитите промахи:- Singleflight: только один процесс перегенерирует значение (см. пример Lua).
- Soft-TTL + background refresh: после `soft_ttl` отдавайте устаревшее (stale) и обновляйте фоном.
- Semaphore/lock: `SET key:lock value NX PX=2000`.
- Near-stale: `stale-while-revalidate` для API ответов (см. раздел 8).
5) Ключи, неймспейсы, сериализация
5.1 Именование ключей
Шаблон: `{domain}:{entity}:{id}:{field}`
Примеры:- `user:profile:42` `catalog:product:1001:v2` `psp:rates:2025-11-03`
Добавляйте версию схемы (`:v2`) — это облегчает массовую инвалидацию.
5.2 Неймспейсы через «версию пространства»
Держите ключ `ns:catalog = 17`. Реальные ключи: `catalog:17:product:1001`. Для глобальной инвалидации каталога просто инкрементируйте `ns:catalog`.
5.3 Сериализация/компрессия
JSON — удобен, но тяжел. Используйте MessagePack/CBOR.
Включайте компрессию (LZ4/ZSTD) для больших payload (>1–2 KB). В Redis — на стороне клиента.
6) Горячие ключи и шардирование
Hot-keys: мониторьте top-N по hit/miss/byte. Для экстремально горячих ключей:- Replicated read pattern: дублируйте значение в несколько shard-ключей `hot:k:1..N`, выбирайте случайный при чтении.
- Local L1: держите в памяти процесса с подпиской на инвалидацию.
- Redis Cluster — нативно (16384 hash-слотов).
- Memcached — клиентский консистентный хеш.
- Hash-tag в Redis `{…}` фиксирует слот для набора ключей: `user:{42}:profile` и `user:{42}:limits` окажутся на одном шарде.
7) Политики вытеснения и размеры
Redis `maxmemory-policy`: `allkeys-lru`, `volatile-lru`, `allkeys-lfu`, `noeviction` и т. д. Для кеша обычно `allkeys-lru`/`allkeys-lfu`.
Memcached — LRU на item-slab.
Размер ключа и value: следите за max item size (Memcached по умолчанию 1 МБ, тюнинг slab).
Превышение памяти должно деградировать предсказуемо: не `noeviction` на активном пути.
maxmemory 32gb maxmemory-policy allkeys-lfu hz 50 tcp-keepalive 60
8) Паттерны защиты от штормов — код
8.1 Redis Lua singleflight (псевдо)
lua
-- KEYS[1] = data_key, KEYS[2] = lock_key
-- ARGV[1] = now_ms, ARGV[2] = soft_ttl_ms, ARGV[3] = hard_ttl_ms, ARGV[4] = lock_ttl_ms local payload = redis. call("GET", KEYS[1])
if payload then local meta = redis. call("HGETALL", KEYS[1].. ":meta")
local last = tonumber(meta[2] or "0")
if tonumber(ARGV[1]) - last < tonumber(ARGV[2]) then return { "HIT", payload }
end if redis. call ("SET," KEYS [2], "1," "NX," "PX," ARGV [4]) then return {"REFRESH," payload} - one worker updates, the rest give stale end return {"STALE," payload}
end if redis. call("SET", KEYS[2], "1", "NX", "PX", ARGV[4]) then return { "MISS", nil }
end return { "BUSY", nil }
8.2 Node.js cache-aside (упрощенно)
js const v = await redis. get(key);
if (v) return decode(v);
const lock = await redis. setNX(key+":lock", "1", { PX: 1500 });
if (lock) {
const fresh = await loadFromDB(id);
await redis. set(key, encode(fresh), { EX: ttl, NX: false });
await redis. del(key+":lock");
return fresh;
} else {
await sleep(60); // short backoff const retry = await redis. get (key) ;//give someone's already filled return decode (retry);
}
9) Инвалидация и согласованность
По событию: при изменении в БД публикуйте `pub/sub` событие `invalidate:{ns}:{id}` → подписчики удаляют ключи.
По таймеру: короткий TTL для часто меняющихся данных.
Версионирование: см. `ns:` ключи.
Outbox: гарантия доставки инвалидации (событие в лог/топик, ретраи).
Идемпотентность операций с кешом: используйте `SETXX/SETNX`, версии (`etag`) и хэш-поля для инкремента.
10) Репликация, кластер, failover
10.1 Redis
Sentinel: автоматический failover master-replica (стейтFUL IP/имя).
Cluster: шардирование + автоматический failover; клиенты должны поддерживать редиректы `MOVED/ASK`.
AOF/RDB: для кеша обычно `appendfsync everysec`, можно без персистентности (как чистый кеш).
10.2 Memcached
Нет репликации из коробки. Надежность — через многосерверный шард + повтор `n` (client-side).
При падении ноды — рост промахов и «переучивание» кеша.
10.3 K8s и сетевые аспекты
Redis/Memcached не любят частую пересоздачу pod’ов; используйте StatefulSet + антиподы по AZ, фиксированные PVC/POD IP.
Ставьте PodDisruptionBudget и TopologySpreadConstraints.
11) Транзакции, скрипты и атомарность (Redis)
INCR/DECR, HINCRBY — счетчики, квоты, rate-limits (только учитывайте persist).
MULTI/EXEC — пачка атомарных команд.
Lua (EVAL) — read-modify-write без гонок.
Pipeline — уменьшает RTT (особенно при сетевом хопе).
lua
-- KEYS[1]=bucket, ARGV[1]=capacity, ARGV[2]=refill_rate_per_sec, ARGV[3]=now_ms
-- Returns 1 if the token is issued, otherwise 0
12) Очереди, pub/sub и Streams (Redis)
Pub/Sub: инвалидация, сигналы. Без сохранения, только онлайн-слушатели.
Streams: очереди событий с подтверждением (ACK), группы потребителей, ретраи — удобно для write-behind/фан-аутов.
Lists (`BRPOP`): простые очереди.
Не используйте Redis как «единую шину всего» без бэкапа — это кеш/быстрая шина, не Kafka.
13) Безопасность и доступ
Изоляция сетей/VPC, mTLS на ingress-уровне, ACL/пароли (`requirepass`/ACL в Redis 6+).
Disable dangerous команд в Redis (`CONFIG`, `FLUSHALL`, `KEYS`) через ACL.
Для Memcached — не слушать публичные интерфейсы, `-U 0` (без UDP), только приватные сети.
PII не хранить; если нужно — короткий TTL + шифрование на уровне приложения.
14) Наблюдаемость и обслуживание
Ключевые метрики:- Hit ratio / Miss ratio (по namespace/маршруту).
- Latency p95/p99 команд `GET/SET/MGET`, timeouts.
- Evictions и OOM errors.
- Replication lag (Redis), cluster state, migrate/rehash events.
- Top-N keys по трафику/байтам (сэмплингом).
- Логи: медленные команды (`slowlog`), ошибки сети.
- Дашборды: общее (CPU/RAM/connections), команды, слоты кластера, sentinels, пропуская через Prometheus-экспортеры.
15) Конфиги и развертывание — примеры
15.1 Redis Sentinel (фрагмент)
port 6379 protected-mode yes appendonly yes appendfsync everysec maxmemory-policy allkeys-lfu
`sentinel.conf`:
sentinel monitor m1 10. 0. 0. 11 6379 2 sentinel auth-pass m1 sentinel down-after-milliseconds m1 5000 sentinel failover-timeout m1 60000
15.2 Redis Cluster (helm values, упрощенно)
yaml cluster:
enabled: true nodes: 6 # 3 masters + 3 replicas persistence:
size: 100Gi resources:
requests: { cpu: "500m", memory: "2Gi" }
15.3 Memcached (deployment)
yaml containers:
- image: memcached:1. 6 args: ["-m", "32768", "-I", "2m", "-v", "-t", "8", "-o", "modern"]
ports: [{ containerPort: 11211 }]
15.4 NGINX как read-through прокси (контур API)
nginx proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=api:100m max_size=10g inactive=10m;
map $request_uri $cache_key { default "api:$request_uri"; }
location /api/ {
proxy_cache api;
proxy_cache_valid 200 1m;
proxy_cache_use_stale updating error timeout http_500 http_502 http_503 http_504;
proxy_cache_lock on; # singleflight на уровне NGINX proxy_cache_key $cache_key;
proxy_pass http://backend;
}
16) Тестирование и гейты
Нагрузочные профили «холодный/теплый/горячий кеш».
Инъекция промахов (purge массово) — origin должен выдержать «переучивание».
Алерты: резкое падение hit-ratio, рост miss latency, лавина evictions, рост timeouts.
17) Анти-паттерны
Хранить «истину» в Redis без AOF/RDB и без резервирования.
TTL=0 (бессрочно) для волатильных данных → вечная неконсистентность.
Массовый `KEYS ` в проде.
Отсутствие jitter/soft-TTL → синхронные истечения и шторма.
Один инстанс на все команды без шардирования/реплик.
Использовать Memcached для задач, требующих атомарности/скриптов.
18) Чек-лист внедрения (0–45 дней)
0–10 дней
Выбрать шаблон (cache-aside + L1/L2), описать ключи, TTL, неймспейсы.
Включить jitter/soft-TTL, singleflight; базовые алерты/дашборды.
Для Redis — настроить ACL, protected-mode, slowlog, maxmemory-policy.
11–25 дней
Перейти на шардирование (Redis Cluster или клиентский хеш), реплики.
Инвалидация через pub/sub или версию неймспейса; outbox в БД.
Нагрузочные тесты «переучивания» кеша; limiting origin.
26–45 дней
Автопромо/канареечные TTL, прогрев перед релизом.
Streams для write-behind/фоновых пересборок.
Еженедельные отчеты по hit-ratio, топ-ключам, стоимости памяти.
19) Метрики зрелости
Hit-ratio L2 ≥ 80% (статистика по маршрутам/неймспейсам).
P95 GET < 2–3 ms (in-DC), промахи < SLO origin.
0 шторма при массовой инвалидации (доказано тестами).
Автоматическая инвалидация и версионирование неймспейсов.
Шардирование/репликация покрывают отказ 1 узла без заметной деградации.
20) Заключение
Сильная архитектура кеша — это дисциплина ключей и TTL, защита от шторма, правильное шардирование и предсказуемое вытеснение. Redis дает богатую семантику, персистентность и атомарность; Memcached — максимум простоты и скорости. Добавьте наблюдаемость, инвалидацию по событиям, L1+L2, и кеш станет ускорителем платформы, а не источником случайных падений и «мистических» багов.