缓存命中率99%依然能把数据库压垮,这事儿听起来像悖论
很多开发者习惯用
需要注意的是,
下一篇
多标签页JWT刷新踩坑 →
IMemoryCache 的 GetOrCreateAsync,觉得这个名字听起来很原子化,以为它能保证只有一个请求去查库。实际上这里面根本没有锁。在高并发场景下,如果一个热点 Key 过期,瞬间涌入 50 个请求,它们会同时发现缓存失效,然后全部触发数据库查询。结果就是:同一个查询在几百毫秒内被重复执行了 50 次。针对这个问题,.NET 9 引入的 HybridCache 才是正解。它在内部实现了并发请求的去重(Deduping),同一个 Key 的并发请求只允许一个去跑工厂方法,其余的全部排队等待结果。
实操对比一下代码就明白了。
传统的 IMemoryCache 写法(危险):
app.MapGet("/products/memory/{id}", async (string id, IMemoryCache cache, FakeDb db) =>
await cache.GetOrCreateAsync($"mem:product:{id}", entry =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
return db.LoadProductAsync(id, flavor: "memory");
}));升级到 HybridCache 后的写法(安全):
首先在服务注册时加入:
builder.Services.AddHybridCache();然后修改 Endpoint:
app.MapGet("/products/hybrid/{id}", async (string id, HybridCache cache, FakeDb db, CancellationToken ct) =>
await cache.GetOrCreateAsync(
$"hyb:product:{id}",
async token => await db.LoadProductAsync(id, flavor: "hybrid"),
new HybridCacheEntryOptions { Expiration = TimeSpan.FromMinutes(5) },
cancellationToken: ct));我写了个简单的 Demo 压测,模拟 50 个并发请求且缓存为空的情况,结果非常离谱:
- IMemoryCache: 产生了 50 次数据库调用。
- HybridCache: 仅产生 1 次数据库调用。
需要注意的是,
HybridCache 并不会缩短首个请求的响应时间(因为还是要查库),但它极大地降低了数据库的压力。在真实生产环境下,50 个重复查询会导致数据库响应变慢,进而拉长缓存失效的时间窗,吸引更多请求进来,形成恶性循环。一个踩坑细节:HybridCache 的 L1 缓存底层其实还是 MemoryCache。如果你在同一个应用里混用两者,千万记得给 Key 加前缀区分,否则可能会因为 Key 冲突导致一些莫名其妙的 Bug。