必须通过 IMemoryCache 接口+依赖注入使用,否则缓存不共享、不响应内存压力、过期策略失效,高并发下易OOM或击穿;MemoryCache.Default属旧框架,手动new会创建孤立实例,导致共享失败、SizeLimit失效、GC无响应。

必须通过 IMemoryCache 接口 + 依赖注入使用,别用 MemoryCache.Default 或手动 new MemoryCache() —— 否则缓存不共享、不响应内存压力、过期策略失效,高并发下极易 OOM 或击穿。
为什么不能直接 new MemoryCache() 或用 MemoryCache.Default
这两个写法绕过了 ASP.NET Core 的 DI 生命周期和统一调度器:MemoryCache.Default 来自 System.Runtime.Caching,和 Microsoft.Extensions.Caching.Memory 完全无关;手动 new MemoryCache(new MemoryCacheOptions()) 会创建孤立实例,多个 Service 注入后拿到的不是同一个缓存对象。
后果包括:
- 缓存项在不同 Controller/Service 间不共享,看似“存了”实则查不到
- 注册时设的
SizeLimit不生效,SetSize()失去意义,内存悄悄涨满 - 过期检查、紧凑(compaction)、GC 压力响应全部失效,OOM 前无预警
- 单元测试中若真要手动实例化,必须显式传入带
SizeLimit和CompactionPercentage的MemoryCacheOptions
GetOrCreate() 是防击穿的唯一可靠方式
GetOrCreate() 把“查 + 存”原子化,内部加锁确保 factory 委托只执行一次。而手写 if (!_cache.TryGetValue()) { _cache.Set() } 在高并发下必然触发重复加载 —— 比如 10 个请求同时发现 key 缺失,就会并行执行 10 次数据库查询。
正确用法要点:
- 过期策略必须在 factory 委托内的
entry上设置:entry.SlidingExpiration = TimeSpan.FromMinutes(5),外部设置无效 - factory 中抛异常,缓存不会写入,调用方直接收到 exception;如需缓存失败结果(如空对象),得在委托里
try/catch后返回 - 若加载逻辑含
await(如异步 DB 查询),必须用GetOrCreateAsync(),否则线程池阻塞
SizeLimit 和 SetSize() 必须双配合才有效
只在 DI 注册时设 options.SizeLimit = 100 * 1024 * 1024(约 100MB)远远不够。所有未调用 SetSize() 的缓存项,默认按大小 1 计算 —— 存一个 15MB 的 byte[],缓存系统却只当它占 1 单位,结果就是内存失控、GC 频繁、服务卡顿。
实操规则:
- 注册时开启限制:
builder.Services.AddMemoryCache(options => options.SizeLimit = 100 * 1024 * 1024) - 大对象(如导出流、大 JSON 字符串)必须显式标大小:
.SetSize(15 * 1024 * 1024) - 小对象建议统一标
.SetSize(1),避免混用导致 size 统计失真 - 别用
sizeof或Marshal.SizeOf算对象大小——它们只算托管头和指针,不算实际堆内存
SlidingExpiration 和 AbsoluteExpirationRelativeToNow 别共存
两者逻辑冲突,运行时会抛 ArgumentException: Both sliding and absolute expiration cannot be set。不是“谁优先谁生效”,而是根本禁止同时设置。
选型建议:
- 配置项、字典表等低频更新数据:用
AbsoluteExpirationRelativeToNow(固定存活时间) - 用户会话态、活跃商品信息等高频访问数据:用
SlidingExpiration(每次访问重置计时) - 需要“最长 30 分钟 + 活跃就续命”?得自己封装逻辑,靠
PostEvictionCallback触发重加载,不能靠混用过期策略
真正容易被忽略的是:缓存对象本身要是线程安全或不可变的。存一个 List<T> 或 Dictionary<K,V>,外部代码直接修改它,所有后续读取都会看到脏数据 —— 这不是缓存失效,是数据污染。


















