下午好🌅🌅🌅
本答案参考通义千问
你描述的问题是一个典型的内存泄漏问题,特别是在 .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 方法中使用了 FromSqlRaw 和 ToListAsync(),这些操作会返回一个 List<AlarmRecordRealResponse> 对象。如果这个列表没有被及时释放或引用,可能导致 内存泄漏。
重点: 如果 GetAlarmRecords 返回的数据没有被正确处理或引用,可能会导致垃圾回收器无法回收这些对象。
3. EF Core 查询性能问题
即使表数据不多,但如果你的查询涉及到大量对象创建和映射(如 Select(a => new AlarmRecordRealResponse { ... })),也可能导致内存占用增加。
✅ 解决方案
🔧 一、替换 System.Threading.Timer 为 IHostedService + 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 Profiler 或 dotMemory 工具进行内存分析。
- 检查是否有其他地方存在对
AlarmRecordRealResponse 的引用,导致其无法被回收。 - 如果你使用的是 MySQL,请确认
MySqlParameter 是否正确传递,避免意外生成额外对象。
如需进一步协助,请提供完整的项目结构或内存快照。