landeli2 2026-01-03 16:24 采纳率: 0%
浏览 10

关于#.netcore#的问题:程序跑一天大概会导致内存增涨1GB空间,长时间运行最终会导致服务器内存被吃完程序崩溃(相关搜索:sql查询|定时器)

环境描述:.netcore webapi程序,.net8 sdk
问题描述:定时器定期(10秒1次查询)查询表内容(表中的内容并不多常态下也就200300条左右记录),导致程序占用内存持续增长,并且GC也回收不了。程序跑一天大概会导致内存增涨12GB空间,长时间运行最终会导致服务器内存被吃完程序崩溃。恳请专家们指教。

public class RegionPeoSumService : IHostedService, IDisposable
{
        public async Task StartAsync(CancellationToken cancellationToken)
        {
            timer = new System.Threading.Timer(async (state) =>
            {
                using (var scope = _scopeFactory.CreateScope())
                {
                    var dbContext = scope.ServiceProvider.GetRequiredService<OpenAuthDBContext>();
                    await _regionPeopleNumApp.GetAlarmRecords(dbContext );
                }
            }, "hello", 5000, 10000);
        }

}


    public class RegionPeopleNumApp : BaseStringApp<Region, OpenAuthDBContext>
    {
       public async Task<List<AlarmRecordReal>> GetAlarmRecords(OpenAuthDBContext context)
       {
           string sql = $@" select Alarm_id,Alarm_type,Alarm_param1,Alarm_param2,Alarm_param3,Alarm_param4,Alarm_param5 from alarmrecordreal ";
           var data = await context.Set<AlarmRecordRealResponse>().FromSqlRaw(sql, new List<MySqlParameter>().ToArray()).Select(a => new AlarmRecordRealResponse
           {
               Alarm_id = a.Alarm_id,
               Alarm_type = a.Alarm_type,
               Alarm_param1 = a.Alarm_param1,
               Alarm_param2 = a.Alarm_param2,
               Alarm_param3 = a.Alarm_param3,
               Alarm_param4 = a.Alarm_param4,
               Alarm_param5 = a.Alarm_param5
           }).ToListAsync();
           return data;
       }
}

  • 写回答

3条回答 默认 最新

  • 檀越@新空间 2026-01-03 16:25
    关注

    下午好🌅🌅🌅
    本答案参考通义千问

    你描述的问题是一个典型的内存泄漏问题,特别是在 .NET Core WebAPI 应用中,由于定时任务(System.Threading.Timer)频繁执行 SQL 查询,导致内存持续增长,最终引发 OOM(Out of Memory) 崩溃。


    ✅ 问题分析

    1. 定时器与作用域的使用不当

    你在 StartAsync 中创建了一个 Timer,并且在每次调用时都通过 using (var scope = _scopeFactory.CreateScope()) 创建了作用域。虽然你使用了 using 语句来确保作用域释放,但 Timer 是一个后台线程,可能无法正确触发 IDisposable 的释放逻辑,特别是当它被多次调度时。

    重点: System.Threading.Timer 并不推荐用于依赖注入服务的场景,因为它不是设计为与 IServiceProvider 配合使用的。

    2. SQL 查询中的对象未被释放

    你在 GetAlarmRecords 方法中使用了 FromSqlRawToListAsync(),这些操作会返回一个 List<AlarmRecordRealResponse> 对象。如果这个列表没有被及时释放或引用,可能导致 内存泄漏

    重点: 如果 GetAlarmRecords 返回的数据没有被正确处理或引用,可能会导致垃圾回收器无法回收这些对象。

    3. EF Core 查询性能问题

    即使表数据不多,但如果你的查询涉及到大量对象创建和映射(如 Select(a => new AlarmRecordRealResponse { ... })),也可能导致内存占用增加。


    ✅ 解决方案

    🔧 一、替换 System.Threading.TimerIHostedService + Task.Delay

    建议使用 IHostedService 实现定时任务,而不是直接使用 System.Threading.Timer。这样可以更好地管理生命周期和依赖注入。

    修改后的代码示例:

    public class RegionPeoSumService : IHostedService, IDisposable
    {
        private Timer _timer;
        private readonly IServiceScopeFactory _scopeFactory;
        private readonly ILogger<RegionPeoSumService> _logger;
    
        public RegionPeoSumService(IServiceScopeFactory scopeFactory, ILogger<RegionPeoSumService> logger)
        {
            _scopeFactory = scopeFactory;
            _logger = logger;
        }
    
        public Task StartAsync(CancellationToken cancellationToken)
        {
            _timer = new Timer(async _ => await DoWork(cancellationToken), null, TimeSpan.Zero, TimeSpan.FromSeconds(10));
            return Task.CompletedTask;
        }
    
        private async Task DoWork(CancellationToken cancellationToken)
        {
            try
            {
                using (var scope = _scopeFactory.CreateScope())
                {
                    var dbContext = scope.ServiceProvider.GetRequiredService<OpenAuthDBContext>();
                    await _regionPeopleNumApp.GetAlarmRecords(dbContext);
                }
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error in RegionPeoSumService");
            }
        }
    
        public Task StopAsync(CancellationToken cancellationToken)
        {
            _timer?.Change(Timeout.Infinite, Timeout.Infinite);
            return Task.CompletedTask;
        }
    
        public void Dispose()
        {
            _timer?.Dispose();
        }
    }
    

    重点: 使用 Timer 替换 System.Threading.Timer,并确保在 StopAsync 中停止计时器,避免重复调用。


    🔧 二、优化 SQL 查询,避免不必要的对象创建

    你的 GetAlarmRecords 方法中存在如下代码:

    .Select(a => new AlarmRecordRealResponse
    {
        Alarm_id = a.Alarm_id,
        ...
    })
    

    这会导致创建一个新的 AlarmRecordRealResponse 对象,而你可以直接返回原始查询结果(假设 AlarmRecordRealResponse 与数据库结构一致)。

    优化后的代码:

    public async Task<List<AlarmRecordReal>> GetAlarmRecords(OpenAuthDBContext context)
    {
        string sql = @"select * from alarmrecordreal";
        var data = await context.Set<AlarmRecordReal>().FromSqlRaw(sql).ToListAsync();
        return data;
    }
    

    重点: 如果 AlarmRecordRealResponse 与数据库字段完全一致,可以直接使用 Set<AlarmRecordReal>() 而非手动映射。


    🔧 三、检查 EF Core 缓存和查询缓存

    有时,EF Core 会缓存查询结果(尤其是 FromSqlRaw)。你可以尝试禁用缓存或明确清理缓存。

    示例代码:

    context.ChangeTracker.Clear(); // 清除上下文跟踪
    

    重点: 在每次查询后调用 ChangeTracker.Clear() 可以防止实体被缓存,从而减少内存占用。


    🔧 四、监控内存使用情况

    建议在程序中加入内存监控逻辑,例如:

    GC.Collect();
    GC.WaitForFullGCComplete();
    long memoryUsed = GC.GetTotalMemory(true);
    _logger.LogInformation($"Current memory usage: {memoryUsed / 1024 / 1024} MB");
    

    重点: 定期监控内存使用,可以帮助你快速定位内存泄漏点。


    ✅ 总结

    | 问题 | 解决方案 | |------|----------| | 定时器使用不当 | 使用 IHostedService + Timer 替代 System.Threading.Timer | | SQL 查询性能差 | 简化查询,避免手动映射 | | 内存泄漏 | 添加 ChangeTracker.Clear(),定期 GC | | 监控不足 | 加入内存监控日志 |


    🧠 补充建议

    • 使用 Visual Studio ProfilerdotMemory 工具进行内存分析。
    • 检查是否有其他地方存在对 AlarmRecordRealResponse 的引用,导致其无法被回收。
    • 如果你使用的是 MySQL,请确认 MySqlParameter 是否正确传递,避免意外生成额外对象。

    如需进一步协助,请提供完整的项目结构或内存快照。

    评论

报告相同问题?

问题事件

  • 创建了问题 1月3日