MemoryCache淘汰需显式启用SizeLimit并合理调用SetSize(),否则仅设过期时间不会释放内存;Sliding与Absolute过期不可混用,PostEvictionCallback仅通知不支持自动续载。

MemoryCache 的淘汰不是“定时扫描+删”,而是惰性触发 + 内存压力驱动,靠 SizeLimit 和 SetSize() 配合才能真正控制回收行为;不设大小限制、不标对象尺寸,就等于没配淘汰策略。
为什么缓存项从不被回收?——SizeLimit 没开或设得太小
默认情况下 MemoryCache 不启用内存大小限制,SizeLimit = null,意味着它永远不会因内存紧张而主动淘汰项。即使你设了过期时间,也只是“到期后不返回”,但内存里还占着,直到 GC 或手动清理。
- 注册时必须显式设
SizeLimit:在Program.cs中写services.AddMemoryCache(options => options.SizeLimit = 1024 * 1024 * 50)(单位是估算字节数,比如 50MB) -
SizeLimit是粗略上限,不是硬隔离;超过后会触发 compact,但不会立刻 OOM - 如果设了
SizeLimit = 1,哪怕只存一个字符串,下次 Set 就可能把前一个挤掉——这是常见误配
大对象缓存后仍被优先淘汰?——SetSize() 没调用或估错
启用 SizeLimit 后,每项的“大小”默认为 1。一个 byte[1024*1024] 和一个 string 都算 “1”,导致淘汰完全不按真实内存占用走。
- 必须对大对象显式调用
.SetSize(1024)(例如 1KB),小对象可统一设为.SetSize(1) - 别用
sizeof或Marshal.SizeOf算——它们只返回托管头大小,不是实际堆内存 - 经验做法:序列化成
byte[]后取.Length,再乘个 1.2 安全系数,作为SetSize()值
SlidingExpiration 和 AbsoluteExpirationRelativeToNow 能否共存?
能编译,但不能共存——逻辑冲突,行为不可控。运行时不报错,但结果必然违背直觉。
- 同时设了
.SetSlidingExpiration(TimeSpan.FromMinutes(10))和.SetAbsoluteExpirationRelativeToNow(TimeSpan.FromMinutes(5)),实际就是 5 分钟后必删,滑动失效 - 高频读+低更新(如配置):只用
AbsoluteExpirationRelativeToNow - 活跃数据(如用户 session):只用
SlidingExpiration - 真要“最长 30 分钟 + 活跃就续命”,得自己封装:用
PostEvictionCallback触发重加载,而不是依赖内置组合
淘汰回调里能 reload 数据吗?——PostEvictionCallback 的局限
PostEvictionCallback 是通知机制,不是重载钩子。它在项被移除后才执行,且不保证线程上下文,也不能阻止淘汰发生。
- 回调中调用
_cache.Set()是安全的,但无法“原地复活”该 key,新值是全新 entry - 若想自动续缓存,必须配合
GetOrCreate()+ 外部状态(比如一个ConcurrentDictionary<string, SemaphoreSlim>控制重载节奏) - 回调里做耗时操作(如查 DB)会拖慢淘汰流程,建议只记日志或发指标,重载逻辑交给业务层异步处理
淘汰策略的有效性,不取决于你写了多少种过期方式,而取决于 SizeLimit 是否开启、SetSize() 是否如实标注、以及是否避开 Sliding 和 Absolute 的混用陷阱——这三个点漏掉任何一个,缓存就只是个带过期时间的字典,不是可控的内存管理器。


















