Skip to content

内存过高排查清单 ​

概述 ​

EF Core 应用出现内存过高(Memory Leak)是常见问题,通常由 ChangeTracker 累积、查询不当或资源未释放导致。本文提供系统化的排查清单和解决方案。


排查清单 ​

✅ 1. 检查 ChangeTracker 累积 ​

问题: 长时间运行的 DbContext 会累积大量跟踪实体

csharp
// ❌ 错误: 处理大量数据时内存爆炸
public async Task ProcessLargeDataset()
{
    using var context = new AppDbContext();
    
    for (int i = 0; i < 100000; i++)
    {
        var product = new Product { Name = $"Product {i}" };
        context.Products.Add(product);
        // ChangeTracker 累积 10 万个实体!
    }
    
    await context.SaveChangesAsync();
}

// ✅ 正确: 分批处理并清理
public async Task ProcessLargeDataset()
{
    using var context = new AppDbContext();
    
    for (int i = 0; i < 100000; i++)
    {
        var product = new Product { Name = $"Product {i}" };
        context.Products.Add(product);
        
        if (i % 1000 == 0)
        {
            await context.SaveChangesAsync();
            context.ChangeTracker.Clear(); // 清理 ChangeTracker
        }
    }
}

检测方法:

csharp
var trackedCount = context.ChangeTracker.Entries().Count();
Console.WriteLine($"Tracked entities: {trackedCount}");

✅ 2. 使用 AsNoTracking 查询 ​

问题: 只读查询不需要跟踪实体

csharp
// ❌ 错误: 不必要的跟踪
var products = await _context.Products.ToListAsync();

// ✅ 正确: 只读查询使用 AsNoTracking
var products = await _context.Products
    .AsNoTracking()
    .ToListAsync();

// 全局设置(推荐)
builder.Services.AddDbContext<AppDbContext>(options =>
{
    options.UseSqlServer(connectionString)
           .UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
});

✅ 3. 避免加载整个表 ​

问题: 一次性加载大量数据到内存

csharp
// ❌ 错误: 加载所有产品
var allProducts = await _context.Products.ToListAsync();

// ✅ 正确: 分页加载
var products = await _context.Products
    .OrderBy(p => p.Id)
    .Skip(pageIndex * pageSize)
    .Take(pageSize)
    .ToListAsync();

// ✅ 更好: 流式处理
await foreach (var product in _context.Products.AsAsyncEnumerable())
{
    ProcessProduct(product); // 逐个处理,不全部加载
}

✅ 4. 使用投影查询 ​

问题: 加载不需要的导航属性

csharp
// ❌ 错误: 加载整个实体
var orders = await _context.Orders
    .Include(o => o.Customer)
    .Include(o => o.Items)
    .ToListAsync();

// ✅ 正确: 只选择需要的字段
var orderDtos = await _context.Orders
    .Select(o => new OrderDto(
        o.Id,
        o.OrderDate,
        o.TotalAmount,
        o.Customer.Name)) // 只加载需要的字段
    .ToListAsync();

✅ 5. 及时释放 DbContext ​

问题: DbContext 未正确释放

csharp
// ❌ 错误: 手动创建但未释放
var context = new AppDbContext();
var products = await context.Products.ToListAsync();
// 忘记调用 context.Dispose()

// ✅ 正确: 使用 using 语句
await using (var context = new AppDbContext())
{
    var products = await context.Products.ToListAsync();
}

// ✅ 更好: 依赖注入自动管理生命周期
public class ProductService
{
    private readonly AppDbContext _context;
    
    public ProductService(AppDbContext context)
    {
        _context = context; // DI 容器负责释放
    }
}

✅ 6. 检查事件订阅泄漏 ​

问题: 事件处理器未取消订阅

csharp
// ❌ 错误: 重复订阅事件
public class EventLeakExample
{
    public void Subscribe(AppDbContext context)
    {
        // 每次调用都会新增一个处理器
        context.SavingChanges += OnSavingChanges;
    }
}

// ✅ 正确: 确保只订阅一次
public class EventProperExample
{
    private bool _subscribed = false;
    
    public void Subscribe(AppDbContext context)
    {
        if (!_subscribed)
        {
            context.SavingChanges += OnSavingChanges;
            _subscribed = true;
        }
    }
}

✅ 7. 避免 N+1 查询 ​

问题: 循环中执行查询导致内存和性能问题

csharp
// ❌ 错误: N+1 查询
var orders = await _context.Orders.ToListAsync();
foreach (var order in orders)
{
    // 每个订单都执行一次查询!
    order.Customer = await _context.Customers
        .FindAsync(order.CustomerId);
}

// ✅ 正确: 使用 Include
var orders = await _context.Orders
    .Include(o => o.Customer)
    .ToListAsync();

✅ 8. 缓存优化 ​

问题: 重复查询相同数据

csharp
// ❌ 错误: 每次都查询数据库
public async Task<Category?> GetCategoryAsync(int id)
{
    return await _context.Categories.FindAsync(id);
}

// ✅ 正确: 使用缓存
private readonly IMemoryCache _cache;

public async Task<Category?> GetCategoryAsync(int id)
{
    return await _cache.GetOrCreateAsync($"category_{id}", async entry =>
    {
        entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10);
        return await _context.Categories.FindAsync(id);
    });
}

诊断工具 ​

1. 内存分析器 ​

bash
# 使用 dotMemory 或 Visual Studio Diagnostic Tools
# 查看内存快照,找出占用最多的对象

2. 监控 ChangeTracker ​

csharp
public class ChangeTrackerMonitor : IDisposable
{
    private readonly AppDbContext _context;
    private readonly ILogger _logger;
    private readonly Timer _timer;
    
    public ChangeTrackerMonitor(AppDbContext context, ILogger logger)
    {
        _context = context;
        _logger = logger;
        
        _timer = new Timer(_ =>
        {
            var count = _context.ChangeTracker.Entries().Count();
            if (count > 1000)
            {
                _logger.LogWarning($"High ChangeTracker count: {count}");
            }
        }, null, TimeSpan.Zero, TimeSpan.FromSeconds(10));
    }
    
    public void Dispose()
    {
        _timer?.Dispose();
    }
}

3. EF Core 日志 ​

csharp
builder.Services.AddDbContext<AppDbContext>(options =>
{
    options.UseSqlServer(connectionString)
           .LogTo(Console.WriteLine, LogLevel.Warning)
           .EnableSensitiveDataLogging();
});

最佳实践总结 ​

优化项影响程度实施难度优先级
ChangeTracker.Clear()⭐⭐⭐⭐⭐ 高⭐ 低🔥 P0
AsNoTracking⭐⭐⭐⭐ 高⭐ 低🔥 P0
分页/流式处理⭐⭐⭐⭐⭐ 高⭐⭐ 中🔥 P0
投影查询⭐⭐⭐⭐ 高⭐⭐ 中P1
及时释放 DbContext⭐⭐⭐⭐⭐ 高⭐ 低🔥 P0
避免 N+1⭐⭐⭐⭐ 高⭐⭐ 中P1
缓存优化⭐⭐⭐ 中⭐⭐⭐ 高P2

快速排查步骤 ​

  1. 监控: 添加 ChangeTracker 计数监控
  2. 分析: 使用内存分析器定位泄漏点
  3. 优化: 应用上述优化策略
  4. 验证: 对比优化前后的内存使用

遵循此清单可解决 90% 以上的 EF Core 内存问题!

基于 MIT 许可发布