Cache write strategies pattern — compare write-through, write-behind, write-around, and cache-aside race conditions side-by-side. Topology: client outside, group with cache (Redis) and database (Postgres). Four scenarios demonstrate different trade-offs between durability, latency, and consistency.
Cache обычно обсуждают через read path: cache-aside, miss, fill, TTL. Но многие реальные проблемы начинаются на write path. Что происходит, когда пользователь меняет цену, inventory count, counter, profile, leaderboard score или analytics aggregate? Писать только в DB и инвалидировать cache? Писать в cache и DB одновременно? Подтверждать клиенту до durable write? Ответ зависит от latency, consistency и допустимой потери данных.
Write-through и write-behind это стратегии, где cache становится активным участником write path. В write-through cache синхронно пишет в database и ack клиенту только после успеха. В write-behind cache принимает write, быстро отвечает клиенту, а database обновляется асинхронно batch-ами или worker-ом. Они выглядят похожими, но имеют противоположные trade-offs: durability и consistency против latency и throughput.
Знать эти паттерны важно, чтобы не применять Redis как магический ускоритель. Cache не отменяет источник истины и не делает write безопасным сам по себе. Если система подтверждает write до durable storage, она должна честно принять риск потери последних updates. Если система требует strong consistency, она должна платить latency синхронной записи или использовать транзакционный механизм.
::concept{slug="cache-aside"} ::concept{slug="transactional-outbox"}
Write-through: клиент пишет в cache layer, cache сразу обновляет себя и database, затем возвращает ack. Cache всегда warm, reads быстрые, DB consistent. Но write latency равна cache operation плюс DB commit, а failure handling сложнее: если DB write failed, ack нельзя отдавать.
Write-behind: клиент пишет в cache, получает ack почти сразу, а flush в DB происходит позже. Это похоже на блокнот у кассира, который обещает вечером занести записи в бухгалтерскую систему. Быстро, удобно, можно batch-ить. Но если блокнот сгорит до переноса, данные потеряны.
Write-around: write идет прямо в DB и не заполняет cache. Это полезно для одноразовых или cold данных, чтобы не вытеснять hot keys. Cache-aside: app пишет в DB и инвалидирует cache, а read miss позже заполнит cache. Cache-aside обычно default, но имеет race windows.
ADR-style framing: выбирай write strategy от бизнес-инварианта. Если потеря write недопустима, ack должен следовать durable commit. Если последние N updates можно восстановить или потерять, write-behind может дать throughput. Если данные редко читаются, не загрязняй cache.
Диаграмма показывает client, Redis cache и Postgres database. Есть прямые ребра client -> cache, client -> db и cache -> db. Это намеренно: разные стратегии используют разные пути. Write-through и write-behind идут через cache к DB. Write-around и часть cache-aside идут напрямую в DB. Cache-aside race показывает, как read miss и concurrent write могут пересечься.
Redis в диаграмме не обязательно literal Redis с persistence. Это cache layer или in-memory data grid. Postgres представляет durable store. Важно не название продукта, а момент ack: когда клиенту сказали [OK], данные уже в durable store или только в volatile/buffered layer.
Диаграмма также показывает latency math. Write-through ack приходит после cache и DB. Write-behind ack приходит после cache, а DB flush позже. Это делает write-behind привлекательным для counters and analytics, но опасным для orders and balances.
write-through-sync показывает синхронный путь. Клиент пишет x=5, cache обновляется, затем Postgres commit, только потом ack. Следующий read получает hot cache. Урок: write-through убирает cache-aside race и держит cache warm, но write path теперь зависит от DB latency and availability.
write-behind-fast-risky показывает быстрый ack и async flush. Cache принимает много writes, background worker batch-ит 100 updates в DB, throughput хороший. Затем cache crash до следующего flush, последние 99 writes потеряны. Урок: write-behind это осознанная продажа durability за latency and batch efficiency.
write-around-cold показывает запись мимо cache. Audit logs или batch import идут прямо в DB и не вытесняют hot profiles/products from cache. Урок: не каждое written data должно становиться cached data. Cache size ограничен, и cold writes могут ухудшить hit ratio.
cache-aside-race показывает classic stale fill. Client A делает miss и читает старое x=1 из DB. Client B обновляет DB на x=2 и инвалидирует cache. Потом Client A late-fill-ит cache старым x=1. Урок: invalidate-after-write не всегда достаточно при concurrent reads; нужны versioning, locks, short TTL или write-through.
ADR-001: write-through против cache-aside. Write-through дает warm cache и меньше stale windows, но делает cache layer частью critical write path. Если cache unavailable, надо решить: block writes, bypass cache или degrade consistency. Cache-aside проще и чаще default: DB остается primary, cache optional. Но race conditions и cold reads надо закрывать TTL, version checks или request coalescing.
ADR-002: write-behind против durable writes. Write-behind дает минимальную latency и amortized DB cost через batching. Это отлично для view counters, analytics aggregates, telemetry, leaderboard approximations. Но ack до durable commit означает possible loss. Для payments, orders, inventory decrement, account balances и legal audit write-behind недопустим без durable queue/WAL.
ADR-003: batch efficiency против freshness. Чем дольше flush interval, тем лучше batching и ниже DB load. Но тем больше lag и loss window. Короткий interval уменьшает риск, но приближает cost к write-through. Практический выбор: flush by size, time and pressure, plus monitoring of lag and failed flushes.
ADR-004: cache as primary против DB as primary. В write-through/write-behind cache layer выглядит как entry point for writes. Но если cache eviction, restart или split-brain возможны, надо ясно определить source of truth. Write-through обычно сохраняет DB primary. Write-behind временно делает cache primary until flush, и это требует persistence, replication или acceptance of loss.
ADR-005: write-around против cache pollution. Write-around сохраняет cache для hot data, но первый read после write будет cold miss. Для data, которую почти сразу читают, это плохо. Для logs, imports, audit trail и archival writes это правильно.
Java caching libraries и enterprise caches часто поддерживают write-through/write-behind adapters к relational DB. Они полезны, когда application code хочет единый cache API, но operational semantics надо документировать явно.
Apache Ignite и Hazelcast могут работать как in-memory data grid с write-behind persistence. Они дают high throughput, но требуют disciplined configuration for persistence, replication and recovery.
Redis часто используют как session/counter/cache layer. Для write-behind с Redis нужен durable mechanism: AOF, streams, queue worker, replication и clear recovery story. Просто SET в volatile Redis и ack клиенту не является durable write.
CDN и edge caches чаще используют write-around/cache-aside semantics для content. Product inventory, prices and balances обычно требуют write-through-like consistency или прямой DB transaction plus invalidation.
Transactional outbox решает смежную проблему: DB commit and event publish должны быть атомарно согласованы. Это часто лучше, чем write-behind через volatile cache, если downstream update must not be lost.
Write-behind для финансовых операций. Потеря последних writes после cache crash превращается в потерянные деньги или несогласованные заказы.
Нет мониторинга flush lag. Система выглядит здоровой, потому что clients получают fast ack, а DB отстает на минуты.
Cache eviction в write-through без понимания read path. Если cache всегда warm только пока хватает memory, eviction возвращает cold misses and DB load.
DB down при write-through не описан. Если DB недоступна, cache уже updated или нет? Клиент получил ack или error? Нужен atomic failure behavior.
Cache-aside invalidate без защиты race. Late fill после invalidate кладет stale value в cache до TTL expiry.
Write-around для данных, которые читаются сразу после записи. Получается постоянный cold miss и лишняя DB нагрузка.
Одинаковая стратегия для всех ключей. Inventory, analytics, audit, profile и feed имеют разные consistency requirements.
Не используй write-behind, если нельзя потерять подтвержденный клиенту write. Для money movement, order state, inventory reservation, auth policy и compliance audit ack должен быть после durable commit.
Не используй write-through, если DB latency делает write SLO невыполнимым, а business допускает eventual consistency. В таких случаях лучше durable queue, outbox или write-behind with persistence.
Не используй cache as primary store без persistence and recovery plan. Если Redis configured as volatile LRU cache, он не должен быть местом, где единственная копия recent writes.
Не используй write-around для hot write-read flows: user обновил profile и сразу читает page. Тут cache invalidation или write-through лучше.
Не используй cache strategy как замену schema design и indexes. Если DB не выдерживает reads/writes из-за плохого model, cache только отложит проблему.
Начни с cache-aside, чтобы понять default read-heavy pattern и его race windows. Затем читай transactional-outbox: он нужен, когда write в DB должен надежно породить async update/event. Для reliability рядом стоят timeout-deadline и retry-backoff, потому что cache flush worker тоже должен иметь budgets and idempotency.
Связанные CloudArch материалы: ::concept{slug="cache-aside"} ::concept{slug="transactional-outbox"} ::concept{slug="timeout-deadline"} ::concept{slug="retry-backoff"}
Внешние источники: System Design Primer cache strategies, Microsoft Cache-Aside pattern, Redis persistence documentation, Apache Ignite write-behind documentation, Martin Kleppmann on logs and data consistency.