Cache-aside: read from the cache, otherwise from the database, and write to the cache.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | // ====================================================================== // Q: How do you handle cache invalidation and cache stampede? // // - Cache-aside: read from the cache, otherwise from the database, and write to // the cache. Always with a TTL, even when there is explicit invalidation, so // that a mistake fixes itself. // - Invalidation on change: when a value changes (e.g. a PriceListUpdated event // from the ERP), delete the related keys or tags. // - Stampede: a popular key expires and 500 requests hit the database at once. // HybridCache solves this on its own (only one factory call per key). // IMemoryCache.GetOrCreateAsync does not. Random jitter on the TTL also helps. // - Multiple instances with IMemoryCache: each one holds a different version of // the data. Solution: Redis, or HybridCache with a short L1 TTL. // What I do NOT cache, or cache very carefully: // - Stock at checkout: a stale value means overselling. In the product list an // approximate value is enough, but at checkout you check the database (Q3). // - Per-customer or per-tenant data without the customerId / tenantId in the // key: data leaks from one customer to another (Q36). // - The cache is an optimization, not the source of truth. If Redis goes down, // the application keeps working (more slowly) from the database. // ====================================================================== // Price list change from the ERP: invalidation by tag public class PriceListUpdatedHandler(HybridCache cache, IOutputCacheStore outputCache) : IDomainEventHandler<PriceListUpdated> { public async Task Handle(PriceListUpdated e, CancellationToken ct) { await cache.RemoveByTagAsync($"prices:{e.PriceListId}", ct); await outputCache.EvictByTagAsync("catalog", ct); } } |