MemoryCache.Get线程安全,但GetOrCreate非原子;应通过DI复用单例实例;过期清理惰性执行;PostEvictionCallback不保证触发。

MemoryCache.Get 是线程安全的,但 GetOrCreate 不是原子操作
MemoryCache.Get 本身是线程安全的,多个线程并发调用不会导致内部状态损坏。但常见误用是先 Get 判空,再 Set,这中间存在竞态窗口:两个线程同时发现缓存为空,都去构造值并写入,造成重复计算和覆盖风险。
正确做法是使用 GetOrCreate 或 GetOrCreateAsync,它们在内部加锁确保“查-算-存”三步原子性。但要注意:GetOrCreate 的 valueFactory 委托会在锁内执行,若构造逻辑耗时(如 IO、复杂计算),会阻塞其他 key 的缓存操作,拖慢整体吞吐。
- 高并发下优先用
GetOrCreateAsync,把耗时构造移到异步委托里,避免阻塞同步线程池 - valueFactory 中不要调用阻塞 API(如
.Result、.Wait()),否则引发死锁或线程饥饿 - 如果 valueFactory 可能抛异常,异常会被捕获并导致本次 GetOrCreate 返回 null(.NET 6+ 行为),需在外层处理 fallback 逻辑
MemoryCache 默认实例不是进程单例,多处 new 就多份缓存
很多人直接 new MemoryCache(new MemoryCacheOptions()),以为拿到的是全局缓存。实际上每次 new 都创建独立实例,互不共享数据,也无跨实例协调机制。这在 Web API 或依赖注入场景中极易导致缓存击穿——每个控制器或服务实例维护自己的缓存副本,无法分摊压力。
正确方式是复用同一个实例。ASP.NET Core 中应通过 DI 注册:
services.AddMemoryCache(options =>
{
options.SizeLimit = 1024; // 启用大小限制需手动设
});
然后在类中注入 IMemoryCache 接口。DI 容器默认以 Singleton 生命周期提供,所有使用者共享同一缓存实例和内部 ConcurrentDictionary。
千博购物系统.Net能够适合不同类型商品,为您提供了一个完整的在线开店解决方案。千博购物系统.Net除了拥有一般网上商店系统所具有的所有功能,还拥有着其它网店系统没有的许多超强功能。千博购物系统.Net适合中小企业和个人快速构建个性化的网上商店。强劲、安全、稳定、易用、免费是它的主要特性。系统由C#及Access/MS SQL开发,是B/S(浏览器/服务器)结构Asp.Net程序。多种独创的技术使
- 手动生成实例仅适用于单元测试或极简脚本,生产环境必须走 DI
- 若需多个隔离缓存(如按租户分区),应显式注册多个命名选项,而非 new 多个实例
- 注意
IMemoryCache是接口,实现类MemoryCache内部用ConcurrentDictionary存储,其线程安全性已由 .NET 保障
缓存项过期与内存压力触发的清理不是实时的
MemoryCache 的过期策略分两种:绝对过期(AbsoluteExpiration)和滑动过期(SlidingExpiration)。但无论哪种,过期检查都不是定时轮询或实时中断,而是“惰性清理”——只有在 Get/Count/遍历时才触发过期扫描。这意味着过期项可能在内存中残留数秒甚至更久,尤其在低访问频率场景下。
更关键的是内存压力响应:当 SizeLimit 被设置且缓存总 size 超限时,MemoryCache 会触发 LRU 清理,但该过程本身也是异步且非抢占式的。它不保证立即释放内存,也不通知调用方哪些项被踢出。
- 不要依赖缓存项“准时消失”,业务逻辑需容忍短暂脏读
- 设置
SizeLimit后必须为每个 entry 指定Size(通过MemoryCacheEntryOptions.Size),否则限流无效 - 监控缓存健康度可读取
MemoryCache.Statistics(需开启options.TrackStatistics = true),但统计本身有轻微开销
自定义过期回调(PostEvictionCallbacks)可能丢失或延迟执行
通过 RegisterPostEvictionCallback 注册的回调函数,在缓存项被移除时触发。但文档明确说明:该回调不保证一定执行,也不保证执行顺序或线程上下文。尤其在进程退出、OOM 或快速批量驱逐时,回调可能被跳过。
典型误用是把回调当“可靠钩子”做资源释放(如关闭文件句柄、注销事件监听)。一旦回调丢失,就会泄漏资源。
- 回调只适合轻量、可丢失的日志记录或指标上报
- 关键资源清理必须在业务代码中显式控制,例如在 Set 缓存前先释放旧资源,或用
IDisposable包装缓存值并在 Get 后手动 Dispose - 回调函数内避免长耗时操作,否则会阻塞缓存内部清理线程









