并行加载下拉列表时DbContext上下文线程冲突问题排查
你遇到的错误本质上是EF Core的核心限制:DbContext实例不是线程安全的,同一时间只能有一个操作在上面执行。虽然你把DbContext设为了Transient生命周期,但你的UnitOfWork模式导致所有仓储共享同一个DbContext实例——当你在LoadProposalsInterfaceDropdownList里并行调用多个仓储的方法时,所有任务都在操作同一个DbContext,这就触发了并发访问的错误。
为什么Transient没起作用?因为你的UnitOfWork实例(不管它的生命周期是Scoped还是其他)在构造时只会注入一次DbContext,之后所有仓储都复用这个实例,所以并行任务还是在同一个DbContext上跑,和DbContext的生命周期配置无关。
要解决这个问题,你需要让每个并行执行的仓储操作使用独立的DbContext实例。最合理的方式是利用IServiceScopeFactory创建独立的作用域,每个作用域内获取专属的仓储(和对应的DbContext)。
修改你的LoadProposalsInterfaceDropdownList方法,注入IServiceScopeFactory并重构代码:
public async Task<DropDownDataList> LoadProposalsInterfaceDropdownList(IServiceScopeFactory scopeFactory) { var data = new DropDownDataList(); // 为每个下拉列表任务创建独立的作用域,确保各自使用单独的DbContext var salesRepTask = Task.Run(async () => { using var scope = scopeFactory.CreateScope(); var salesRepRepo = scope.ServiceProvider.GetRequiredService<ISalesRepRepository>(); return await salesRepRepo.GetDropDownListNoTracking(); }); var proposalStatusTask = Task.Run(async () => { using var scope = scopeFactory.CreateScope(); var statusRepo = scope.ServiceProvider.GetRequiredService<IProposalStatusRepository>(); return await statusRepo.GetDropDownListNoTracking(); }); var sourceListTask = Task.Run(async () => { using var scope = scopeFactory.CreateScope(); var sourceRepo = scope.ServiceProvider.GetRequiredService<ISourceRepository>(); return await sourceRepo.GetDropDownListNoTracking(); }); // 重复上述模式创建其他下拉列表的任务 var yearOptionsTask = Task.Run(async () => { using var scope = scopeFactory.CreateScope(); var yearRepo = scope.ServiceProvider.GetRequiredService<IYearRepository>(); return await yearRepo.GetDropDownListNoTracking(); }); var outForBidTask = Task.Run(async () => { using var scope = scopeFactory.CreateScope(); var bidRepo = scope.ServiceProvider.GetRequiredService<ICompBidsRepository>(); return await bidRepo.GetDropDownListNoTracking(); }); var infoSecAwareTask = Task.Run(async () => { using var scope = scopeFactory.CreateScope(); var infoSecRepo = scope.ServiceProvider.GetRequiredService<IInfoSecAwareRepository>(); return await infoSecRepo.GetDropDownListNoTracking(); }); var timeDriverTask = Task.Run(async () => { using var scope = scopeFactory.CreateScope(); var timeDriverRepo = scope.ServiceProvider.GetRequiredService<ITimeDriverRepository>(); return await timeDriverRepo.GetDropDownListNoTracking(); }); var needsDriverTask = Task.Run(async () => { using var scope = scopeFactory.CreateScope(); var needsDriverRepo = scope.ServiceProvider.GetRequiredService<INeedsDriverRepository>(); return await needsDriverRepo.GetDropDownListNoTracking(); }); // 等待所有任务完成 await Task.WhenAll( salesRepTask, proposalStatusTask, sourceListTask, yearOptionsTask, outForBidTask, infoSecAwareTask, timeDriverTask, needsDriverTask); // 赋值结果 data.SalesRepList = await salesRepTask as IReadOnlyList<DropDownList>; data.ProposalStatusList = await proposalStatusTask; data.SourceList = await sourceListTask; data.YearOptionsList = await yearOptionsTask; data.OutForBidList = await outForBidTask; data.InfoSecAwareList = await infoSecAwareTask; data.TimeDriverList = await timeDriverTask; data.NeedsDriverList = await needsDriverTask; return data; }
缓存下拉列表数据:下拉列表数据通常是不频繁变更的静态数据,你可以把这些数据缓存到内存(比如用
IMemoryCache),第一次加载后直接读缓存,完全避免并行加载的需求,性能提升会更明显。检查UnitOfWork的生命周期:确保你的
UnitOfWork注册为Scoped生命周期(而不是Singleton),因为Singleton的UnitOfWork会持有DbContext实例很久,不仅有并发问题,还会导致数据过期。注册代码类似:builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();避免过度并行:如果数据库服务器资源有限,过多的并行查询反而可能导致性能下降,你可以根据实际情况调整并行任务的数量,或者使用
Task.WhenAll但控制并发度。
内容的提问来源于stack exchange,提问作者John DK

