0

0

c# MemoryCache 的线程安全问题和高并发使用

月夜之吻

月夜之吻

发布时间:2026-01-08 08:45:10

|

193人浏览过

|

来源于php中文网

原创

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

c# memorycache 的线程安全问题和高并发使用

MemoryCache.Get 是线程安全的,但 GetOrCreate 不是原子操作

MemoryCache.Get 本身是线程安全的,多个线程并发调用不会导致内部状态损坏。但常见误用是先 Get 判空,再 Set,这中间存在竞态窗口:两个线程同时发现缓存为空,都去构造值并写入,造成重复计算和覆盖风险。

正确做法是使用 GetOrCreateGetOrCreateAsync,它们在内部加锁确保“查-算-存”三步原子性。但要注意: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能够适合不同类型商品,为您提供了一个完整的在线开店解决方案。千博购物系统.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
  • 回调函数内避免长耗时操作,否则会阻塞缓存内部清理线程
缓存的线程安全边界很清晰,但高并发下的行为偏差往往来自对“原子性范围”和“惰性机制”的误判。最常被忽略的是 valueFactory 的执行时机和 PostEvictionCallback 的不可靠性——这两处不深挖文档,很容易在线上压测时突然暴露问题。

相关专题

更多
c语言中null和NULL的区别
c语言中null和NULL的区别

c语言中null和NULL的区别是:null是C语言中的一个宏定义,通常用来表示一个空指针,可以用于初始化指针变量,或者在条件语句中判断指针是否为空;NULL是C语言中的一个预定义常量,通常用来表示一个空值,用于表示一个空的指针、空的指针数组或者空的结构体指针。

231

2023.09.22

java中null的用法
java中null的用法

在Java中,null表示一个引用类型的变量不指向任何对象。可以将null赋值给任何引用类型的变量,包括类、接口、数组、字符串等。想了解更多null的相关内容,可以阅读本专题下面的文章。

435

2024.03.01

counta和count的区别
counta和count的区别

Count函数用于计算指定范围内数字的个数,而CountA函数用于计算指定范围内非空单元格的个数。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

195

2023.11.20

硬盘接口类型介绍
硬盘接口类型介绍

硬盘接口类型有IDE、SATA、SCSI、Fibre Channel、USB、eSATA、mSATA、PCIe等等。详细介绍:1、IDE接口是一种并行接口,主要用于连接硬盘和光驱等设备,它主要有两种类型:ATA和ATAPI,IDE接口已经逐渐被SATA接口;2、SATA接口是一种串行接口,相较于IDE接口,它具有更高的传输速度、更低的功耗和更小的体积;3、SCSI接口等等。

1006

2023.10.19

PHP接口编写教程
PHP接口编写教程

本专题整合了PHP接口编写教程,阅读专题下面的文章了解更多详细内容。

56

2025.10.17

php8.4实现接口限流的教程
php8.4实现接口限流的教程

PHP8.4本身不内置限流功能,需借助Redis(令牌桶)或Swoole(漏桶)实现;文件锁因I/O瓶颈、无跨机共享、秒级精度等缺陷不适用高并发场景。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

345

2025.12.29

线程和进程的区别
线程和进程的区别

线程和进程的区别:线程是进程的一部分,用于实现并发和并行操作,而线程共享进程的资源,通信更方便快捷,切换开销较小。本专题为大家提供线程和进程区别相关的各种文章、以及下载和课程。

476

2023.08.10

java学习网站推荐汇总
java学习网站推荐汇总

本专题整合了java学习网站相关内容,阅读专题下面的文章了解更多详细内容。

3

2026.01.08

java学习网站汇总
java学习网站汇总

本专题整合了java学习网站相关内容,阅读专题下面的文章了解更多详细内容。

0

2026.01.08

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Django 教程
Django 教程

共28课时 | 2.9万人学习

Excel 教程
Excel 教程

共162课时 | 11.1万人学习

SciPy 教程
SciPy 教程

共10课时 | 1.1万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号