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

EF DatabaseContext注入自定义Scoped服务后性能低下问题排查

EF Core在Hosted Service中性能优化方案

我看了你遇到的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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.28 04:14:59