Cache Coherence concept page: multi-level invalidation across browser/CDN/app cache/DB, write strategies trade-offs (write-through vs write-back vs write-around), invalidation race conditions, cache stampede protection via single-flight, multi-DC propagation via CDC + Kafka. Five scenarios covering write-through happy path, write-back data loss risk, invalidation race, stampede + single-flight fix, and multi-DC eventual coherence window.
«Cache invalidation is one of the two hard things in computer science» (Phil Karlton). Когда у вас одна кеш-нода — invalidation это просто DEL key. Когда multi-level (browser → CDN edge → reverse proxy → app cache → DB), либо distributed (Memcached cluster, Redis Cluster, multi-DC), один update может оставить stale данные в пяти местах одновременно — и каждый клиент увидит свою версию правды.
Cache coherence — это поддержание согласованности между копиями. В CPU за это отвечает hardware (MESI protocol через bus snooping). В распределённых кешах общей шины нет — есть software-протоколы и trade-offs. Понять их = знать, почему пользователь видит свой старый аватар через 30 секунд после смены, и как этого избежать без выключения кеша вообще.
Cache coherence — это не один протокол, а спектр компромиссов между скоростью, согласованностью и стоимостью координации. Идеальной consistency не бывает; есть выбор окна неконсистентности (staleness window) под бизнес-требование.
Финансы (balances, inventory, payment state) терпят миллисекунды staleness, но не минуты — stale = double-spend, oversell, реальные деньги. Социальная лента (likes, view counts, аватары) терпит минуты — stale = «обновится при следующем reload», никто не умер. Подбор стратегии = подбор окна под домен.
Три измерения, по которым выбираете протокол:
Типичный production-стек кеширования с full invalidation pipeline:
Edge layer: три CDN-PoP (US/EU/ASIA), каждый со своим кешем — независимый, eventually consistent через purge API.
Origin DC: Load Balancer → App (с ADR-001 про выбор строгости coherence) → Redis (TTL 60s) + Postgres. App пишет в обе стороны: cache и DB.
Invalidation pipeline: Postgres WAL → CDC (Debezium) → Kafka → Invalidator. Invalidator делает DEL в Redis и PURGE во все CDN-edge. Этот путь — единственный способ из write в одном DC дойти до cache в другом без cross-DC synchronous replication.
Edges идут только в одну сторону по физическому потоку (browser → CDN → LB → app → cache/db → CDC → Kafka → invalidator → cache+CDN). Ответы анимируются reverse-direction по тем же edges.
Synchronous write-through: app пишет в cache и в DB на каждом запросе. Read сразу после write всегда видит свежие данные на origin tier (~16ms total). Это «верхняя граница» coherence в пределах одного DC — strong consistency для всех reads через origin. Цена: writes медленнее (нужно дождаться двух систем), и при cross-DC ситуация сложнее (см. multi-dc сценарий).
Use case: balances, inventory, payment state — где «соврать» хуже, чем «отказать».
Write-back: app возвращает ack клиенту после write в cache, а в DB сливает асинхронно через background worker. Latency для клиента — суб-миллисекунды (vs 16ms у write-through). Но если Redis падает до flush — последние N writes теряются навсегда (нет WAL, нет durability). Counter откатывается на ту цифру, которая успела попасть в DB.
Use case OK: view counts, leaderboards, analytics — tolerable loss. Use case NEVER: balances, orders, payments.
Классическая race condition в cache-aside: Client A делает GET (miss), стартует медленный SELECT (80ms). Параллельно Client B делает UPDATE + DEL cache. Затем A получает старый snapshot из DB (он начал read до B's update) и пишет его в cache после B's DEL. Результат: cache содержит stale до TTL, все следующие reads (Client C, D, ...) видят старую версию.
Это главная причина почему «invalidate-on-write» не работает в одиночку. Фиксы: write-through (атомарно с DB write), CAS / version check на SET (SET only if version >= last seen), короткий TTL (5-30s) как defence-in-depth.
Hot key (feed
, 10K rps) expires одновременно для всех клиентов → 1000 concurrent misses → 1000 identical SELECT → DB CPU 100% → cascade failure. Это «thundering herd».Решение — single-flight: distributed lock на recompute. Первый процесс делает SETNX recompute:feed:home, выигрывает lock, идёт в DB. Остальные 999 ждут (или возвращают stale). Победитель пишет результат + удаляет lock → waiters читают cache. 1 DB query вместо 1000.
Альтернативы: refresh-ahead (проактивное обновление до TTL), stale-while-revalidate (отдай stale сразу, обнови в фоне — HTTP RFC 5861, Next.js ISR), probabilistic early refresh.
Write в DC-EU должен дойти до cache в DC-US без synchronous cross-DC replication (она бы убила write latency). Путь: EU-DB → WAL → Debezium CDC → Kafka → US-invalidator → DEL в US-Redis + PURGE во все CDN edge.
Типичное окно staleness: 100-500ms p50, до 30s p99 для CDN edge. Во время окна US-клиенты видят старое. Это фундаментальная цена eventual consistency между DC — её нельзя «починить» без перехода на synchronous replication (которая добавляет 50-200ms к каждому write).
Решение зашито в app ноде (ADR-001): выбор строгости coherence зависит от домена.
Финансы / inventory / auth: write-through synchronous + explicit invalidation + короткий TTL (5s) как defence-in-depth. Cache на том же DC, что DB. Single-master writes, replicas только для reads с RYW guarantee (sticky session к master в течение N секунд после write).
Социальный feed / профили / leaderboards: cache-aside + TTL (30-300s) + tag-based invalidation. Multi-DC eventual consistency через CDC + Kafka. CDN soft-purge (mark stale + revalidate) вместо hard-purge — чтобы не вызвать origin stampede. Stale-while-revalidate для UX: всегда отдай что-то быстро, обнови в фоне.
Главные оси trade-off:
| Стратегия | Write latency | Consistency window | Risk |
|---|---|---|---|
| Write-through sync | Высокий (~16ms) | ~0 в DC, 200ms-30s cross-DC | DB+cache должны быть live |
| Write-back async | Низкий (<2ms) | 0 в cache, секунды до DB | Data loss при cache crash |
| Cache-aside + TTL | Низкий | До TTL (минуты) | Race на fill, stale на write |
| Versioned keys | Низкий | 0 (immutable) | Cache bloat, LRU pressure |
xkey: user-42 avatar).max-age=31536000 без версии в URL — пользователи навсегда залипают на старом JS после deploy. Используйте /static/app.<hash>.js.Не используйте multi-level cache, если:
Не путайте cache coherence с replication consistency. Это разные вещи: replication — между копиями БД одного типа, coherence — между разными tier'ами (cache, DB, edge). Решения иногда похожи (CDC, event bus), но trade-offs разные.