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

.NET Core 3+EF+HostedService:首次删数据无效,二次运行才生效

问题根源分析

你猜的没错,核心问题确实和同一依赖注入作用域下的DbContext行为有关,再加上同步阻塞调用异步方法的坑,才导致了首次运行删除无效的情况:

  1. EF Core DbContext的跟踪缓存特性
    DbContext是有状态的容器,会跟踪所有通过它添加或查询的实体。在你最初的单作用域代码里:

    • 调用CreateMail添加邮件后,这些实体立刻被DbContext的内存缓存(一级缓存)跟踪
    • 紧接着调用GetMailsForSend时,EF Core会直接返回缓存里的实体,不会重新查询数据库
    • 虽然你执行了DeleteMail并调用SaveChangesAsync,但在同一个DbContext实例中,缓存的实体状态更新可能和数据库操作不同步,导致后续逻辑拿到的还是旧状态的实体
  2. 同步阻塞异步方法的风险
    你在同步的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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.07 19:42:51