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

EF Core Context引发内存泄漏问题排查求助

ASP.NET Core Web API内存泄漏问题排查(EF Core + SQLite + Quartz)

问题背景

开发的ASP.NET Core Web API采用依赖注入,结合EF Core与SQLite数据库,存在内存泄漏问题。应用通过Quartz实现的后台任务每9秒执行一次,运行一周后内存占用达1GB,内存转储分析显示,运行约8小时后生成了两千多个TeslaSolarChargerContext实例。

DbContext定义

public class TeslaSolarChargerContext : DbContext, ITeslaSolarChargerContext
{
    public DbSet<ChargePrice> ChargePrices { get; set; } = null!;
    public DbSet<HandledCharge> HandledCharges { get; set; } = null!;
    public DbSet<PowerDistribution> PowerDistributions { get; set; } = null!;

    public string DbPath { get; }

    public void RejectChanges()
    {
        foreach (var entry in ChangeTracker.Entries())
        {
            switch (entry.State)
            {
                case EntityState.Modified:
                case EntityState.Deleted:
                    entry.State = EntityState.Modified; //Revert changes made to deleted entity.
                    entry.State = EntityState.Unchanged;
                    break;

                case EntityState.Added:
                    entry.State = EntityState.Detached;
                    break;
            }
        }
    }

    public TeslaSolarChargerContext()
    {
    }

    public TeslaSolarChargerContext(DbContextOptions<TeslaSolarChargerContext> options)
        : base(options)
    {
    }
}

DbContext接口定义

public interface ITeslaSolarChargerContext
{
    DbSet<ChargePrice> ChargePrices { get; set; }
    DbSet<HandledCharge> HandledCharges { get; set; }
    DbSet<PowerDistribution> PowerDistributions { get; set; }
    ChangeTracker ChangeTracker { get; }
    Task<int> SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken());
    DatabaseFacade Database { get; }
    void RejectChanges();
}

Program.cs依赖注入配置

builder.Services.AddDbContext<ITeslaSolarChargerContext, TeslaSolarChargerContext>((provider, options) =>
    {
        options.UseSqlite(provider.GetRequiredService<IDbConnectionStringHelper>().GetTeslaSolarChargerDbPath());
        options.EnableSensitiveDataLogging();
        options.EnableDetailedErrors();
    }, ServiceLifetime.Transient, ServiceLifetime.Transient)
    .AddTransient<IChargingCostService, ChargingCostService>();
builder.Services
    .AddSingleton<JobManager>()
    .AddTransient<PowerDistributionAddJob>()
    .AddTransient<IJobFactory, JobFactory>()
    .AddTransient<ISchedulerFactory, StdSchedulerFactory>();

单例JobManager实现

public class JobManager
{
    private readonly ILogger<JobManager> _logger;
    private readonly IJobFactory _jobFactory;
    private readonly ISchedulerFactory _schedulerFactory;
    private readonly IConfigurationWrapper _configurationWrapper;

    private IScheduler _scheduler;

    
    public JobManager(ILogger<JobManager> logger, IJobFactory jobFactory, ISchedulerFactory schedulerFactory, IConfigurationWrapper configurationWrapper)
    {
        _logger = logger;
        _jobFactory = jobFactory;
        _schedulerFactory = schedulerFactory;
        _configurationWrapper = configurationWrapper;
    }

    public async Task StartJobs()
    {
        _logger.LogTrace("{Method}()", nameof(StartJobs));
        _scheduler = _schedulerFactory.GetScheduler().GetAwaiter().GetResult();
        _scheduler.JobFactory = _jobFactory;

        var powerDistributionAddJob = JobBuilder.Create<PowerDistributionAddJob>().Build();

        var powerDistributionAddTrigger = TriggerBuilder.Create()
            .WithSchedule(SimpleScheduleBuilder.RepeatSecondlyForever((int)_configurationWrapper.JobIntervall().TotalSeconds)).Build();


        var triggersAndJobs = new Dictionary<IJobDetail, IReadOnlyCollection<ITrigger>>
        {
            {powerDistributionAddJob, new HashSet<ITrigger> {powerDistributionAddTrigger}},
        };

        await _scheduler.ScheduleJobs(triggersAndJobs, false).ConfigureAwait(false);

        await _scheduler.Start().ConfigureAwait(false);
    }

    public async Task StopJobs()
    {
        await _scheduler.Shutdown(true).ConfigureAwait(false);
    }
}

Quartz任务实现

[DisallowConcurrentExecution]
public class PowerDistributionAddJob : IJob
{
    private readonly ILogger<ChargeTimeUpdateJob> _logger;
    private readonly IChargingCostService _service;

    public PowerDistributionAddJob(ILogger<ChargeTimeUpdateJob> logger, IChargingCostService service)
    {
        _logger = logger;
        _service = service;
    }
    public async Task Execute(IJobExecutionContext context)
    {
        _logger.LogTrace("{method}({context})", nameof(Execute), context);
        await _service.AddPowerDistributionForAllChargingCars().ConfigureAwait(false);
    }
}

业务服务构造注入DbContext

public ChargingCostService(ILogger<ChargingCostService> logger,
        ITeslaSolarChargerContext teslaSolarChargerContext)
{
    _logger = logger;
    _teslaSolarChargerContext = teslaSolarChargerContext;
}

服务中EF Core调用代码

var chargePrice = await _teslaSolarChargerContext.ChargePrices
                                                 .FirstOrDefaultAsync().ConfigureAwait(false);

内存泄漏原因分析及解决办法

核心问题:单例Scheduler持有Transient JobFactory导致依赖无法回收

Quartz的Scheduler被单例JobManager持有,而你将IJobFactory注册为Transient生命周期。当_scheduler.JobFactory = _jobFactory;执行后,单例Scheduler会长期持有这个Transient的JobFactory实例,导致JobFactory无法被GC回收。JobFactory创建的Job实例、Job依赖的ChargingCostService以及服务持有的DbContext,都会被这个被持有的JobFactory间接引用,最终造成大量DbContext实例堆积。

解决步骤

  1. 调整JobFactory生命周期为Singleton
    修改Program.cs中的注册代码:

    builder.Services
        .AddSingleton<JobManager>()
        .AddTransient<PowerDistributionAddJob>()
        .AddSingleton<IJobFactory, JobFactory>() // 改为Singleton
        .AddTransient<ISchedulerFactory, StdSchedulerFactory>();
    
  2. 使用服务范围创建Job实例
    自定义JobFactory时,通过IServiceScopeFactory创建独立的服务范围,确保每次Job执行时的依赖都能在执行完毕后被释放:

    public class JobFactory : IJobFactory
    {
        private readonly IServiceScopeFactory _scopeFactory;
    
        public JobFactory(IServiceScopeFactory scopeFactory)
        {
            _scopeFactory = scopeFactory;
        }
    
        public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler)
        {
            var scope = _scopeFactory.CreateScope();
            try
            {
                var job = scope.ServiceProvider.GetRequiredService(bundle.JobDetail.JobType) as IJob;
                // 绑定scope到Job数据映射,后续通过IJobListener释放
                scheduler.Context.Put($"JobScope_{bundle.JobDetail.Key}", scope);
                return job;
            }
            catch
            {
                scope.Dispose();
                throw;
            }
        }
    
        public void ReturnJob(IJob job)
        {
            // 无需手动释放,由IJobListener处理
        }
    }
    

    同时添加JobListener在执行完毕后释放scope:

    public class JobScopeCleanupListener : IJobListener
    {
        public string Name => "JobScopeCleanupListener";
    
        public Task JobExecutionVetoed(IJobExecutionContext context, CancellationToken cancellationToken)
        {
            CleanupScope(context);
            return Task.CompletedTask;
        }
    
        public Task JobToBeExecuted(IJobExecutionContext context, CancellationToken cancellationToken)
        {
            return Task.CompletedTask;
        }
    
        public Task JobWasExecuted(IJobExecutionContext context, JobExecutionException jobException, CancellationToken cancellationToken)
        {
            CleanupScope(context);
            return Task.CompletedTask;
        }
    
        private void CleanupScope(IJobExecutionContext context)
        {
            if (context.Scheduler.Context.TryRemove($"JobScope_{context.JobDetail.Key}", out var scopeObj) && scopeObj is IServiceScope scope)
            {
                scope.Dispose();
            }
        }
    }
    

    在JobManager的StartJobs方法中注册Listener:

    await _scheduler.ListenerManager.AddJobListener(new JobScopeCleanupListener()).ConfigureAwait(false);
    
  3. 恢复DbContext的Scoped生命周期
    EF Core推荐使用Scoped生命周期,修改AddDbContext的配置:

    builder.Services.AddDbContext<ITeslaSolarChargerContext, TeslaSolarChargerContext>((provider, options) =>
        {
            options.UseSqlite(provider.GetRequiredService<IDbConnectionStringHelper>().GetTeslaSolarChargerDbPath());
            options.EnableSensitiveDataLogging();
            options.EnableDetailedErrors();
        }, ServiceLifetime.Scoped, ServiceLifetime.Scoped)
        .AddScoped<IChargingCostService, ChargingCostService>();
    
  4. 修正Job中的日志注入类型
    将PowerDistributionAddJob的日志注入改为匹配自身类型:

    public PowerDistributionAddJob(ILogger<PowerDistributionAddJob> logger, IChargingCostService service)
    {
        _logger = logger;
        _service = service;
    }
    

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.11 08:20:27