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

Blazor应用BackgroundService无法感知API提交的数据库更新问题

问题根因

问题出在你对EF Core DbContext的生命周期使用错误:

  • 你在ExecuteAsync方法启动时只创建了一次服务作用域,从中获取的MyDbContext实例被全程复用,直到BackgroundService停止才会释放
  • EF Core的DbContext默认开启变更跟踪,已经被上下文跟踪过的实体,后续查询会直接返回本地缓存中的版本,不会重新从数据库拉取最新值
  • API控制器每次收到请求都会使用新的DbContext实例写入数据库更新,但后台服务里长期持有的DbContext完全感知不到数据库的变更,一直返回缓存的旧数据,所以读不到修改后的IP等字段值
  • 这种长期持有DbContext的写法本身违反EF Core的设计规范:DbContext是为短生命周期场景设计的,长期复用会导致跟踪缓存持续膨胀引发内存泄漏,还会提升并发冲突概率。

WebAssembly客户端能正常读到更新,是因为客户端每次调用API都会获得全新的DbContext实例,查询直接读取数据库最新值,不存在缓存脏读问题。

修复方案

正确做法是每一轮轮询都创建独立的服务作用域,获取全新的DbContext实例,本轮轮询结束后随作用域一起释放,既解决脏读问题,也符合EF Core的生命周期约定。修正后的Poller代码如下:

public class Poller : BackgroundService
{
    private readonly IServiceScopeFactory _scopeFactory;

    public Poller(IServiceScopeFactory scopeFactory)
    {
        _scopeFactory = scopeFactory;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        // 循环执行轮询逻辑
        while (!stoppingToken.IsCancellationRequested)
        {
            // 为当前轮询周期创建独立服务作用域
            using var scope = _scopeFactory.CreateScope();
            var db = scope.ServiceProvider.GetRequiredService<MyDbContext>();

            // 查询最新的Worker列表,使用异步方法避免阻塞
            var workers = await db.Workers
                .Where(w => w.Status != WorkerStatus.Updating)
                .ToListAsync(stoppingToken);
            
            foreach (var worker in workers)
            {
                var prevStatus = worker.Status;
                // 执行远程状态轮询
                string currentStatus = await worker.PollAsync(stoppingToken);
                if (currentStatus != null)
                {
                    worker.Status = currentStatus;
                }
                if (worker.Status != prevStatus)
                {
                    await Log(db, worker, $"Status changed to {worker.Status}", stoppingToken);
                }

                await db.SaveChangesAsync(stoppingToken);
            }

            // 按需配置轮询间隔,避免无间隔循环打满数据库和CPU资源
            await Task.Delay(TimeSpan.FromSeconds(你的轮询间隔秒数), stoppingToken);
        }
    }
    
    private async Task Log(MyDbContext db, Worker? worker, string text, CancellationToken token)
    {
        var entry = new LogEntry
        {
            Worker = worker,
            Text = text
        };
        db.LogEntries.Add(entry);
        await db.SaveChangesAsync(token);
    }
}
注意事项
  • 永远不要在单例生命周期的服务(比如BackgroundService)中直接注入Scoped生命周期的服务(比如EF Core DbContext),这会产生Captive Dependency问题,必须通过IServiceScopeFactory按需创建作用域获取
  • 不推荐为了复用DbContext使用AsNoTracking()或者手动调用ChangeTracker.Clear()的方案,这类方案只能临时解决脏读问题,无法规避长期持有DbContext带来的内存泄漏、并发冲突等隐患
  • 你的原有代码没有给轮询循环加延迟,会导致代码以最大频率查询数据库,极易引发数据库性能问题,务必根据业务需求配置合理的轮询间隔

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 01:15:39