EF DatabaseContext注入自定义Scoped服务后性能低下问题排查
我看了你遇到的EF Core在Hosted Service中处理消息性能低下的问题,结合你的代码和测试数据,这里有几个针对性的优化方案,应该能大幅提升处理速度:
1. 批量插入减少数据库往返
EF Core每次单独执行AddAsync+SaveChangesAsync都会发送一条INSERT语句到数据库,频繁的网络IO和数据库提交是性能瓶颈的核心原因。改成批量添加后,一次SaveChanges就能提交多条记录,能显著降低开销。
修改Adder类实现批量插入
如果你的消息消费支持缓存一批数据再处理,可以这样修改:
namespace EasyRabbit.RabbitSubscribers { public class Adder : IScopedProcessingService<CalculatorInputs>, IHostedService { private readonly ILogger<Adder> _logger; private readonly ApplicationDbContext _db; private readonly List<Calculation> _batch = new List<Calculation>(50); // 缓存50条再提交 private readonly SemaphoreSlim _batchLock = new SemaphoreSlim(1, 1); public Adder(ApplicationDbContext dbContext, ILogger<Adder> logger) { _logger = logger; _db = dbContext; // 全局禁用变更跟踪,仅针对插入场景 _db.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; } public async Task HandleMessageAsync(CalculatorInputs message) { var calculation = new Calculation() { FirstNumber = message.FirstNumber, SecondNumber = message.SecondNumber, Result = message.FirstNumber + message.SecondNumber }; await _batchLock.WaitAsync(); try { _batch.Add(calculation); if (_batch.Count >= 50) { await _db.Calculations.AddRangeAsync(_batch); await _db.SaveChangesAsync(); _batch.Clear(); } } finally { _batchLock.Release(); } } // 服务停止时处理剩余未提交的批量数据 public async Task StopAsync(CancellationToken cancellationToken) { await _batchLock.WaitAsync(cancellationToken); try { if (_batch.Count > 0) { await _db.Calculations.AddRangeAsync(_batch, cancellationToken); await _db.SaveChangesAsync(cancellationToken); } } finally { _batchLock.Release(); } } public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask; } }
如果你的RabbitMQ订阅支持批量拉取消息(比如一次性获取N条),直接整批处理的性能会更优。
2. 禁用变更跟踪降低EF内部开销
因为你只需要插入数据,不需要EF跟踪实体的变更状态,禁用跟踪能减少EF的内存占用和CPU开销:
方式一:全局配置(Startup.cs中)
services.AddDbContext<ApplicationDbContext>(options => { options.UseSqlServer("你的连接字符串") .UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking); });
方式二:局部配置(仅针对插入操作)
如果其他地方需要变更跟踪,可以在插入时手动设置实体状态:
// 替代AddAsync,避免自动跟踪 _db.Entry(calculation).State = EntityState.Added;
3. 确保Hosted Service中Scope的正确创建
你的GenericHostedSubscriber是单例服务,必须为每个消息处理创建独立的Scope,确保每个Adder实例都有专属的DbContext,避免因复用DbContext导致的变更跟踪缓存膨胀:
检查你的GenericHostedSubscriber实现,应该类似这样:
public class GenericHostedSubscriber<T> : IHostedService { private readonly IServiceScopeFactory _scopeFactory; // 其他依赖... public GenericHostedSubscriber(IServiceScopeFactory scopeFactory) { _scopeFactory = scopeFactory; } private async Task ProcessMessage(T message) { using (var scope = _scopeFactory.CreateScope()) { var processor = scope.ServiceProvider.GetRequiredService<IScopedProcessingService<T>>(); await processor.HandleMessageAsync(message); } } // 其他订阅逻辑... }
4. 使用EF原生SQL执行批量插入(接近ADO.NET性能)
如果批量插入仍达不到预期,可以用EF的原生SQL执行批量插入,和ADO.NET性能接近但保留EF的便利性:
public async Task HandleBatchAsync(List<CalculatorInputs> messages) { var parameters = new List<SqlParameter>(); var valuePlaceholders = new List<string>(); for (int i = 0; i < messages.Count; i++) { var msg = messages[i]; valuePlaceholders.Add($"(@p{i}_1, @p{i}_2, @p{i}_3)"); parameters.Add(new SqlParameter($"@p{i}_1", msg.FirstNumber)); parameters.Add(new SqlParameter($"@p{i}_2", msg.SecondNumber)); parameters.Add(new SqlParameter($"@p{i}_3", msg.FirstNumber + msg.SecondNumber)); } var sql = $"INSERT INTO Calculations (FirstNumber, SecondNumber, Result) VALUES {string.Join(", ", valuePlaceholders)}"; await _db.Database.ExecuteSqlRawAsync(sql, parameters.ToArray()); }
5. 优化数据库连接池配置
调整连接字符串的连接池参数,避免连接不足导致的等待:
Server=localhost; Database=RabbitTest; User ID=sa; Password=Admin1234; Max Pool Size=200;
如果不需要同时在一个连接上执行多个查询,可以去掉MultipleActiveResultSets=true,也能小幅提升性能。
预期效果
结合批量插入+禁用变更跟踪这两个核心优化,性能应该能提升到1000条/秒以上,接近你用ADO.NET的测试结果。你可以逐步测试每个优化的效果,找到最适合你的组合。
内容的提问来源于stack exchange,提问作者suchoss

