.NET Core 3+EF+HostedService:首次删数据无效,二次运行才生效
问题根源分析
你猜的没错,核心问题确实和同一依赖注入作用域下的DbContext行为有关,再加上同步阻塞调用异步方法的坑,才导致了首次运行删除无效的情况:
EF Core DbContext的跟踪缓存特性
DbContext是有状态的容器,会跟踪所有通过它添加或查询的实体。在你最初的单作用域代码里:- 调用
CreateMail添加邮件后,这些实体立刻被DbContext的内存缓存(一级缓存)跟踪 - 紧接着调用
GetMailsForSend时,EF Core会直接返回缓存里的实体,不会重新查询数据库 - 虽然你执行了
DeleteMail并调用SaveChangesAsync,但在同一个DbContext实例中,缓存的实体状态更新可能和数据库操作不同步,导致后续逻辑拿到的还是旧状态的实体
- 调用
同步阻塞异步方法的风险
你在同步的DoWork方法里用GetAwaiter().GetResult()阻塞调用异步EF Core方法,这在ASP.NET Core环境中很容易引发线程死锁或异步操作无法正常完成——这很可能是首次运行时删除操作没真正提交到数据库的直接原因。
拆分作用域后,两个独立的作用域使用全新的DbContext实例:
- 第一个作用域完成邮件转移后释放DbContext,确保异步写入操作正常提交
- 第二个作用域的DbContext会直接查询数据库最新状态,删除操作也能顺利生效
代码优化方案
针对这些问题,我们可以从几个维度优化代码,让它更健壮、符合ASP.NET Core的异步编程规范:
1. 把定时器回调改成异步方法,避免同步阻塞
Timer的回调可以用async void(注意异常处理),彻底摒弃阻塞式调用:
private async void DoWork(object state) { try { // 拆分作用域处理邮件转移 using(var scope = _serviceScopeFactory.CreateScope()) { var cacheService = scope.ServiceProvider.GetRequiredService<ICacheService>(); var mailRepo = scope.ServiceProvider.GetRequiredService<IMailRepo>(); var tmpMails = await cacheService.GetPendingMailsAsync(); foreach(var mail in tmpMails) { await mailRepo.CreateMail(mail); } await cacheService.ClearPendingMailsAsync(); } // 独立作用域处理邮件发送与删除 using(var scope = _serviceScopeFactory.CreateScope()) { var mailRepo = scope.ServiceProvider.GetRequiredService<IMailRepo>(); var mailService = scope.ServiceProvider.GetRequiredService<IMailService>(); var mails = await mailRepo.GetMailsForSend(); foreach(var mail in mails) { bool sendSuccess = await mailService.SendMailAsync(mail); if(sendSuccess) { await mailRepo.DeleteMail(mail.Id); // 改用主键删除更可靠 } else { // 更新重试时间,避免重复发送失败邮件 mail.NextTry = DateTime.Now.AddMinutes(5); await mailRepo.UpdateMailAsync(mail); } } } } catch(Exception ex) { // 务必记录异常,方便排查问题 _logger.LogError(ex, "邮件处理任务执行失败"); } }
2. 优化DbContext的查询行为,减少不必要的跟踪
如果查询只是为了读取数据(比如获取待发送邮件),可以用AsNoTracking()关闭实体跟踪,避免缓存干扰,还能提升性能:
public async Task<List<Mail>> GetMailsForSend() { return await _context.Set<Mail>() .Where(m => m.NextTry <= DateTime.Now) .AsNoTracking() // 不跟踪实体,直接从数据库取最新数据 .ToListAsync(); }
注意:用AsNoTracking()查询的实体不会被DbContext跟踪,删除时建议改用主键操作,避免Attach实体的额外步骤:
public async Task<bool> DeleteMail(Guid mailId) { var mail = await _context.Set<Mail>().FindAsync(mailId); if(mail == null) return false; _context.Set<Mail>().Remove(mail); await _context.SaveChangesAsync(); return true; }
3. 改用BackgroundService基类,替代手动Timer
ASP.NET Core提供的BackgroundService是实现定时任务更规范的方式,自带异步支持和生命周期管理,不需要手动处理Timer:
public class MailProcessingService : BackgroundService { private readonly IServiceScopeFactory _serviceScopeFactory; private readonly ILogger<MailProcessingService> _logger; private readonly int _intervalSeconds = 60; public MailProcessingService(IServiceScopeFactory serviceScopeFactory, ILogger<MailProcessingService> logger) { _serviceScopeFactory = serviceScopeFactory; _logger = logger; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { _logger.LogInformation("邮件处理服务已启动"); while(!stoppingToken.IsCancellationRequested) { try { await ProcessMailsAsync(); } catch(Exception ex) { _logger.LogError(ex, "邮件处理任务执行异常"); } // 等待下一次执行,支持取消信号 await Task.Delay(TimeSpan.FromSeconds(_intervalSeconds), stoppingToken); } _logger.LogInformation("邮件处理服务已停止"); } // 把原来的DoWork逻辑移到这里,完全异步化 private async Task ProcessMailsAsync() { using(var scope = _serviceScopeFactory.CreateScope()) { var cacheService = scope.ServiceProvider.GetRequiredService<ICacheService>(); var mailRepo = scope.ServiceProvider.GetRequiredService<IMailRepo>(); var tmpMails = await cacheService.GetPendingMailsAsync(); foreach(var mail in tmpMails) { await mailRepo.CreateMail(mail); } await cacheService.ClearPendingMailsAsync(); } using(var scope = _serviceScopeFactory.CreateScope()) { var mailRepo = scope.ServiceProvider.GetRequiredService<IMailRepo>(); var mailService = scope.ServiceProvider.GetRequiredService<IMailService>(); var mails = await mailRepo.GetMailsForSend(); foreach(var mail in mails) { bool sendSuccess = await mailService.SendMailAsync(mail); if(sendSuccess) { await mailRepo.DeleteMail(mail.Id); } else { mail.NextTry = DateTime.Now.AddMinutes(5); await mailRepo.UpdateMailAsync(mail); } } } } }
4. 可选:添加事务保障原子性
如果邮件转移和缓存清空需要原子性(要么都成功,要么都回滚),可以在第一个作用域里添加数据库事务:
using(var scope = _serviceScopeFactory.CreateScope()) { var cacheService = scope.ServiceProvider.GetRequiredService<ICacheService>(); var mailRepo = scope.ServiceProvider.GetRequiredService<IMailRepo>(); var context = scope.ServiceProvider.GetRequiredService<AppDbContext>(); using(var transaction = await context.Database.BeginTransactionAsync()) { try { var tmpMails = await cacheService.GetPendingMailsAsync(); foreach(var mail in tmpMails) { await mailRepo.CreateMail(mail); } await cacheService.ClearPendingMailsAsync(); await transaction.CommitAsync(); } catch { await transaction.RollbackAsync(); throw; } } }
内容的提问来源于stack exchange,提问作者mpateck
相关产品推荐
相关产品推荐

