MemoryCache.Get线程安全,但GetOrCreate非原子;应通过DI复用单例实例;过期清理惰性执行;PostEvictionCallback不保证触发。
MemoryCache.Get 本身是线程安全的,多个线程并发调用不会导致内部状态损坏。但常见误用是先 Get 判空,再 Set,这中间存在竞态窗口:两个线程同时发现缓存为空,都去构造值并写入,造成重复计算和覆盖风险。
正确做法是使用 GetOrCreate 或 GetOrCreateAsync,它们在内部加锁确保“查-算-存”三步原子性。但要注意:GetOrCreate 的 valueFactory 委托会在锁内执行,若构造逻辑耗时(如 IO、复杂计算),会阻塞其他 key 的缓存操作,拖慢整体吞吐。
GetOrCreateAsync,把耗时构造移到异步委托里,避免阻塞同步线程池.Result、.Wait()),否则引发死锁或线程饥饿很多人直接 new MemoryCache(new MemoryCacheOptions()),以为拿到的是全局缓存。实际上每次 new 都创建独立实例,互不共享数据,也无跨实例协调机制。这在 Web API 或依赖注入场景中极易导致缓存击穿——每个控制器或服务实例维护自己的缓存副本,无法分摊压力。
正确方式是复用同一个实例。ASP.NET Core 中应通过 DI 注册:
services.AddMemoryCache(options =>
{
options.SizeLimit = 1024; // 启用大小限制需手动设
});
然后在类中注入 IMemoryCache 接口。DI 容器默认以 Singleton 生命周期提供,所有使用者共享同一缓存实例和内部 ConcurrentDictionary。
IMemoryCache 是接口,实现类 MemoryCache 内部用 ConcurrentDictionary 存储,其线程安全性已由 .NET 保障MemoryCache 的过期策略分两种:绝对过
期(AbsoluteExpiration)和滑动过期(SlidingExpiration)。但无论哪种,过期检查都不是定时轮询或实时中断,而是“惰性清理”——只有在 Get/Count/遍历时才触发过期扫描。这意味着过期项可能在内存中残留数秒甚至更久,尤其在低访问频率场景下。
更关键的是内存压力响应:当 SizeLimit 被设置且缓存总 size 超限时,MemoryCache 会触发 LRU 清理,但该过程本身也是异步且非抢占式的。它不保证立即释放内存,也不通知调用方哪些项被踢出。
SizeLimit 后必须为每个 entry 指定 Size(通过 MemoryCacheEntryOptions.Size),否则限流无效MemoryCache.Statistics(需开启 options.TrackStatistics = true),但统计本身有轻微开销通过 RegisterPostEvictionCallback 注册的回调函数,在缓存项被移除时触发。但文档明确说明:该回调不保证一定执行,也不保证执行顺序或线程上下文。尤其在进程退出、OOM 或快速批量驱逐时,回调可能被跳过。
典型误用是把回调当“可靠钩子”做资源释放(如关闭文件句柄、注销事件监听)。一旦回调丢失,就会泄漏资源。
IDisposable 包装缓存值并在 Get 后手动 Dispose