ASP.NET Core应用中DbContext并发问题的排查与解决求助
解决ASP.NET Core后台任务中DbContext并发问题
问题根源
EF Core的DbContext实例非线程安全,你的代码中多个后台任务同时共享同一个DbContext实例,导致并发操作冲突。即使尝试了DbContextFactory,若仍在多任务间传递或共享上下文实例,依然会触发该错误。
正确解决方案
每个后台任务必须使用独立的DbContext实例,具体修改如下:
1. 重构BackgroundJobService依赖注入
移除构造函数中直接注入的BookStoreContext,仅保留IDbContextFactory<BookStoreContext>及其他必要服务:
public class BackgroundJobService : IBackgroundJobService { private readonly ILogger<BackgroundJobService> _logger; private readonly IDbContextFactory<BookStoreContext> _contextFactory; private readonly IFileHandleService _fileService; private readonly IOcrService _ocrService; public BackgroundJobService(IDbContextFactory<BookStoreContext> contextFactory, IFileHandleService fileService, IOcrService ocrService, ILogger<BackgroundJobService> logger) { _logger = logger; _contextFactory = contextFactory; _fileService = fileService; _ocrService = ocrService; } }
2. 修改RunOCRJob方法
仅在获取待处理章节列表时创建上下文,且只加载必要字段(避免不必要的关联查询),后续每个任务独立创建上下文处理:
public async Task RunOCRJob() { _logger.LogInformation("Starting OCR job."); // 获取待处理的章节ID和路径(仅加载必要数据) List<ChapterOCRViewModel> toRunOCR; using (var context = _contextFactory.CreateDbContext()) { toRunOCR = await context.BookChapters .Where(c => !c.IsOcrRequested && c.Book.ChapterExtracted) .OrderBy(o => o.Id) .Take(20) .Select(c => new ChapterOCRViewModel(c.Id, c.Path)) .ToListAsync(); } // 为每个章节创建独立的后台任务 var ocrTasks = toRunOCR.Select(chapter => RunBackgroundJob(chapter)).ToList(); await Task.WhenAll(ocrTasks); _logger.LogInformation("OCR job completed."); }
3. 修改RunBackgroundJob方法
每个任务内部独立创建DbContext实例,完成OCR后更新数据并独立保存:
public async Task RunBackgroundJob(ChapterOCRViewModel chapter) { try { // OCR处理(无数据库操作) var chapterFilePath = _fileService.GetChapterFilePath(chapter.Path); var pageContent = await _ocrService.ReadTextFromPdfAsync(chapterFilePath); // 独立创建上下文处理当前章节的数据库更新 using (var context = _contextFactory.CreateDbContext()) { var chapterEntity = await context.BookChapters .Include(cont => cont.Content) .FirstOrDefaultAsync(c => c.Id == chapter.Id); if (chapterEntity == null) { _logger.LogWarning($"Chapter ID: {chapter.Id} not found."); return; } if (chapterEntity.Content == null) chapterEntity.Content = new ChapterContent(); chapterEntity.Content.Text = pageContent; chapterEntity.OcrAt = DateTime.UtcNow; chapterEntity.Status = StatusEnum.ScheduledIndex; chapterEntity.IsOcrRequested = true; // 当前任务独立保存更改 await context.SaveChangesAsync(); _logger.LogInformation($"OCR for Chapter ID: {chapter.Id} completed successfully."); } } catch (Exception ex) { _logger.LogError($"Error while OCR for Chapter ID: {chapter.Id}. Error message: {ex.Message}"); } }
关键改动说明
- 上下文隔离:每个后台任务在自己的
using块中创建DbContext,确保任务间上下文完全独立,避免并发冲突。 - 数据拆分:获取待处理列表时仅加载必要字段(ID和路径),减少内存占用,且后续任务重新查询实体,避免上下文跟踪问题。
- 独立保存:每个任务完成后立即保存自己的更改,不需要等待所有任务完成再统一保存,降低锁冲突概率。
额外优化建议
- 可以为OCR任务添加批量标记:在获取待处理章节时,先将
IsOcrRequested设为true并保存(用事务保证原子性),避免重复抓取同一章节。 - 控制并发任务数量:通过
SemaphoreSlim限制同时运行的OCR任务数,避免数据库连接池耗尽。
内容的提问来源于stack exchange,提问作者Rasik
相关产品推荐
相关产品推荐

