You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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的场景
  • 如何复现该问题
  • 对应的修复方式

根因说明

该问题由两个因素共同导致:

  1. 程序集版本加载冲突:项目虽然引用了v6.0.0版本的缓存扩展包,但运行时实际加载的是v3.1.0版本的GetOrCreateAsync实现。
  2. v3.1.0版本已知并发竞态bug:v3.1.0的GetOrCreateAsync内部锁逻辑存在缺陷,当多个线程同时触发同一个缓存键的未命中回源时,第一个线程进入委托执行回源创建缓存值的过程中,缓存会先插入一个null占位条目;其他等待锁的线程在锁释放后,不会二次校验缓存条目是否完成赋值,存在概率直接读取到这个null占位值并返回,最终导致后续调用TryGetValue时触发空引用。这也是问题无法稳定复现的核心原因——只有当缓存刚好过期、多个请求同时命中同一个缓存键触发回源时才会触发。

复现方式

可以通过以下步骤稳定复现问题:

  • 将目标缓存的过期时间设置为极短值(如100ms)
  • 在缓存回源委托中增加100ms左右的延迟,模拟数据库查询耗时
  • 使用压测工具对依赖该缓存的接口发起持续高并发请求,通常运行1~2分钟即可复现foosDictionary为null触发的空引用异常。

修复方案

  1. 解决程序集版本冲突
    将项目中所有Microsoft.Extensions.Caching相关NuGet包统一升级,或在项目配置中添加正确的程序集绑定重定向,确保运行时加载v6.0.0版本的缓存扩展实现——v6及以上版本已经修复了上述并发竞态bug,不会再出现返回null占位值的问题。
  2. 增加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))
    {
        // 业务逻辑处理
    }
    
  3. 缓存使用优化建议
    不建议长期缓存可变集合对象,缓存过期时间内如果存在数据更新,需要主动同步更新缓存值,避免脏数据;如果后续切换为分布式缓存,注意集合对象序列化/反序列化的兼容性配置。

内容的提问来源于stack exchange,提问作者Nikita Kalimov

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.26 22:18:14