ASP.NET Core IMemoryCache.GetOrCreateAsync返回null空引用异常排查
ASP.NET Core 3.1 中 MemoryCache GetOrCreateAsync 偶发返回null导致空引用问题排查
问题现象
ASP.NET Core 3.1项目中,代码行if (foosDictionary.TryGetValue(id, out var foo))频繁抛出NullReferenceException,异常堆栈定位到CustomClass.cs第900行(即上述TryGetValue调用行),问题无法稳定复现。
核心业务逻辑
- 通过
_cache.GetOrCreateAsync<Dictionary<Guid, Foo>>获取键为cacheKey的缓存字典,设置缓存绝对过期时间为15秒 - 缓存未命中时,调用仓储层
repository.GetFoos()(基于Dapper.QueryAsync实现)获取IEnumerable<Foo>集合,通过ToDictionary()转换为Dictionary<Guid, Foo>存入缓存 - 若缓存获取的字典中不存在目标id对应数据,则重新拉取数据生成新字典返回
已排除诱因
await repository.GetFoos()返回null:手动硬编码模拟该方法返回null时,异常触发位置与当前堆栈不符.ToDictionary()源集合为null:该场景下异常会直接抛出,堆栈指向GetOrCreateAsync调用处,与当前异常位置不匹配- 多线程并发导致当前线程持有引用被置空
初步排查结论
初步判定异常由GetOrCreateAsync方法返回null导致,但无法定位返回null的具体原因:按照方法预期逻辑,缓存键无对应值时会执行委托创建新值,生成字典的委托不可能返回null;内存缓存通过services.AddMemoryCache()注册,项目引用的GetOrCreateAsync扩展方法版本为v6.0.0,实际运行时加载的实现版本为v3.1.0。
待确认问题:
GetOrCreateAsync是否存在返回null的场景- 如何复现该问题
- 对应的修复方式
根因说明
该问题由两个因素共同导致:
- 程序集版本加载冲突:项目虽然引用了v6.0.0版本的缓存扩展包,但运行时实际加载的是v3.1.0版本的
GetOrCreateAsync实现。 - v3.1.0版本已知并发竞态bug:v3.1.0的
GetOrCreateAsync内部锁逻辑存在缺陷,当多个线程同时触发同一个缓存键的未命中回源时,第一个线程进入委托执行回源创建缓存值的过程中,缓存会先插入一个null占位条目;其他等待锁的线程在锁释放后,不会二次校验缓存条目是否完成赋值,存在概率直接读取到这个null占位值并返回,最终导致后续调用TryGetValue时触发空引用。这也是问题无法稳定复现的核心原因——只有当缓存刚好过期、多个请求同时命中同一个缓存键触发回源时才会触发。
复现方式
可以通过以下步骤稳定复现问题:
- 将目标缓存的过期时间设置为极短值(如100ms)
- 在缓存回源委托中增加100ms左右的延迟,模拟数据库查询耗时
- 使用压测工具对依赖该缓存的接口发起持续高并发请求,通常运行1~2分钟即可复现
foosDictionary为null触发的空引用异常。
修复方案
- 解决程序集版本冲突
将项目中所有Microsoft.Extensions.Caching相关NuGet包统一升级,或在项目配置中添加正确的程序集绑定重定向,确保运行时加载v6.0.0版本的缓存扩展实现——v6及以上版本已经修复了上述并发竞态bug,不会再出现返回null占位值的问题。 - 增加null值兜底校验(全版本兼容防护)
无论是否升级版本,都建议对GetOrCreateAsync的返回值增加null校验,遇到null时主动清除无效缓存、重新执行回源逻辑,从根源上避免空引用,参考代码如下:var cacheKey = "cacheKey"; var foosDictionary = await _cache.GetOrCreateAsync<Dictionary<Guid, Foo>>(cacheKey, async entry => { entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(15); var foos = await repository.GetFoos(); return foos.ToDictionary(f => f.Id); }); // 兜底处理null返回场景,规避版本bug/异常状态 if (foosDictionary == null) { _cache.Remove(cacheKey); var foos = await repository.GetFoos(); foosDictionary = foos.ToDictionary(f => f.Id); _cache.Set(cacheKey, foosDictionary, TimeSpan.FromSeconds(15)); } // 后续执行业务判断 if (foosDictionary.TryGetValue(id, out var foo)) { // 业务逻辑处理 } - 缓存使用优化建议
不建议长期缓存可变集合对象,缓存过期时间内如果存在数据更新,需要主动同步更新缓存值,避免脏数据;如果后续切换为分布式缓存,注意集合对象序列化/反序列化的兼容性配置。
内容的提问来源于stack exchange,提问作者Nikita Kalimov
相关产品推荐
相关产品推荐

