缓存预热需在应用启动后、首个请求前同步完成,避免缓存击穿;分布式环境必须使用带版本号的 IDistributedCache 并禁用滑动过期;数据更新须执行“删-更-延时再删”三步法,并用 SemaphoreSlim 控制并发重建。
高并发下,如果等第一个请求触发缓存加载,必然导致大量线程争抢初始化(即“缓存击穿”),尤其当 GetOrAdd 用的是非线程安全的工厂函数时,可能重复执行耗时操作。正确做法是:在 Program.cs 或 Startup.ConfigureServices 中显式调用预热逻辑,并确保其同步阻塞到完成。
Task.Run(() => PreheatCache()).Wait() 强制同步等待(注意不要在 ASP.NET Core 的同步上下文里用 .Result,易死锁)IHttpContextAccessor 等请求作用域服务;改用 IServiceScopeFactory 创建独立 scopeawait Task.Delay(1) 防止单次占用主线程太久,但整体仍需在 WebApplication 构建完成前结束单机 MemoryCache 在多实例部署下完全失效。必须切换为 IDistributedCache(如 Redis),且不能直接存原始对象——否则多个节点同时更新会覆盖彼此,造成脏数据。
$"user:profile:v2:{userId}",其中 v2 是业务版本号,每次数据结构变更就升级,强制全量刷新distributedCache.SetStringAsync(key, json, new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(2) }),禁用滑动过期(SlidingExpiration),防止“越用越旧”
调用 distributedCache.RemoveAsync(key),而不是等过期;删除失败要记录告警,不可静默忽略“先更DB,再删缓存”在并发更新时有概率导致缓存残留旧值(DB 更新成功,但删缓存失败或被覆盖)。生产环境必须用“删除-更新-延迟再删”三步法。
distributedCache.RemoveAsync(key)
SaveChangesAsync()
await Task.Delay(TimeSpan.FromSeconds(500)) 后再次 RemoveAsync(key) —— 覆盖因主从延迟、重试机制导致的缓存回写try/catch,失败时写入本地队列(如 ConcurrentQueue),由后台服务重试,不能丢弃即使做了预热,缓存过期瞬间仍可能有上百请求同时发现缓存为空,全部涌入 DB。不能靠 GetOrCreateAsync 默认行为扛住——它的 factory 函数不是原子的。
ConcurrentDictionary,键为 cache key,值为独占信号量cache.TryGetValue(key, out var value);若为空,则 semaphores.GetOrAdd(key, _ => new SemaphoreSlim(1,1)).WaitAsync()
Release() 并移除该 semaphore(避免内存泄漏)lock,它跨进程无效;也不要复用同一个 SemaphoreSlim 实例,会导致不同 key 互相阻塞private static readonly ConcurrentDictionary缓存键设计、删除时机、并发重建这三点一旦漏掉任意一个,高并发下的数据不一致就会变成偶发性线上事故——而这类问题往往在压测时不出,上线后半夜爆发。_semaphores = new(); public async Task GetProfileAsync(int userId) { var key = $"user:profile:v2:{userId}"; if (_cache.TryGetValue(key, out UserProfile profile)) return profile; var semaphore = _semaphores.GetOrAdd(key, _ => new SemaphoreSlim(1, 1)); await semaphore.WaitAsync(); try { // Double-check after acquiring semaphore if (_cache.TryGetValue(key, out profile)) return profile; profile = await _db.Users.FirstAsync(u => u.Id == userId); await _cache.SetStringAsync(key, JsonSerializer.Serialize(profile), new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30) }); return profile; } finally { semaphore.Release(); _semaphores.TryRemove(key, out _); } }