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

EF Core继承CachedRepository引发System.InvalidOperationException异常

问题解决:DbContext并发操作异常

问题根源

你的代码里,GetSomeEntity方法在调用GetCacheItem时,提前启动了EF Core查询任务(func(id)直接执行FirstOrDefaultAsync()并返回Task)。当多个请求同时访问同一个未缓存的key时,多个查询Task会共享同一个Scoped生命周期的DbContext,触发并发操作DbContext的异常。

解决方案

1. 修改缓存方法参数为延迟执行的委托

将GetCacheItem的参数从Task<T>改为Func<Task<T>>,确保只有当缓存不存在时才启动DB查询,同时添加缓存锁避免并发查询(缓存击穿):

public class CachedRepository
{
    private readonly IMemoryCache cache;
    const int expirationTime = 20;

    public CachedRepository(IMemoryCache cache)
    {
        this.cache = cache;
    }

    protected async Task<T> GetCacheItem<T>(Func<Task<T>> fetchData, string key, int expirationMinutes = expirationTime)
    {
        string cacheKey = $"{key}";

        // 第一次检查缓存
        if (cache.TryGetValue(cacheKey, out T cacheItem))
        {
            return cacheItem;
        }

        // 创建缓存锁,防止并发击穿DB
        using var lockEntry = cache.CreateEntry($"lock_{cacheKey}");
        lockEntry.SetValue(true);
        lockEntry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(5); // 设置锁过期时间,避免死锁

        // 二次检查缓存:等待锁期间可能已经被其他请求写入缓存
        if (cache.TryGetValue(cacheKey, out cacheItem))
        {
            return cacheItem;
        }

        // 执行DB查询(此时只有一个请求在执行)
        cacheItem = await fetchData();

        // 写入缓存
        cache.Set(cacheKey, cacheItem, DateTimeOffset.UtcNow.AddMinutes(expirationMinutes));
        return cacheItem;
    }
}

2. 修改仓储方法的调用方式

传递延迟执行的委托,而不是提前启动的Task:

public class SomeEntityRepository : CachedRepository
{
    private readonly SomeContext context;
    private readonly string repoName;

    public SomeEntityRepository (SomeContext context, IMemoryCache cache) : base(cache)
    {
        this.context = context;
        this.repoName = GetType().FullName;
    }

    public async Task<SomeEntity> GetSomeEntity(int id)
    {
        const string methodName = nameof(GetSomeEntity);
        string cacheKey = $"{repoName}_{methodName}_{id.ToString()}";

        // 传递Func<Task<SomeEntity>>,而非已启动的Task
        return await GetCacheItem(() => context.SomeEntities
                                                 .AsNoTracking()
                                                 .Where(x => x.Id == id)
                                                 .FirstOrDefaultAsync(), 
                               cacheKey);
    }
}

补充说明

  • 不要将DbContext改为Transient生命周期:这会导致上下文实例过多,耗尽数据库连接池,反而引发新问题。Scoped生命周期是Web应用中DbContext的正确选择。
  • 缓存锁的作用:当缓存失效时,确保只有一个请求去查询DB,其他请求等待缓存写入后直接读取,既避免DbContext并发,也减轻DB压力。

内容的提问来源于stack exchange,提问作者Kent Kostelac

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.03 20:25:06