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
相关产品推荐
相关产品推荐

