为什么通过 entity framework in buckets 从 DB 中获取大量数据时进程内存增加

Why the process memory increases when fetching a lot of data from DB via entity framework in buckets

在循环的每次迭代中的以下代码中,我执行以下操作:

  1. 从数据库中获取 5000 个实体
  2. 根据 FilterRelationsToDelete 中的逻辑,我决定删除哪些实体
  3. 我将要删除的实体的 ID 添加到集合中
  4. 完成循环后,我根据 idsToDelete 集合从数据库中删除实体。

我在visual studio“诊断工具”中看到,进程的内存在每次循环迭代开始时都在上升,在迭代结束后它会减少一半,我的问题是有时它会上升到800MB下降到400MB,有时稳定在200MB,有时超过1GB下降到500MB稳定。

我不确定为什么当数据从数据库到达时,我的进程内存在 200MB 上不稳定并出现小峰值。这可能是什么原因?也许 Entity framework 没有释放它使用的所有内存?也许我故意在这里激活的 GC 没有像我预期的那样清理所有内存?也许我这里有一个我不知道的错误?

我在idsToDelete中累积的longs内存列表几乎为零,这不是问题所在。 有什么方法可以更好地编写这段代码吗?

private static void PlayWithMemory()
{
    int buketSize = 5000;
    List<long> idsToDelete = new List<long>();
    for (int i = 0; i < 500; i++)
    {
        System.GC.Collect();//added just for this example
        using (var context = new PayeeRelationsContext())
        {
            int toSkip = i * bucketSize;
            List<PayeeRelation> dbPayeeRelations = GetDBRelations(context, toSkip, buketSize);
            var relationsToDelete = FilterRelationsToDelete(dbPayeeRelations);
            List<long> ids = relationsToDelete.Select(x => x.id).ToList();
            idsToDelete.AddRange(ids);
            Console.WriteLine($"i = {i}, toSkip = {toSkip}, payeeRelations.Count = {payeeRelationsIds.Count}");
        }
    }
}

private static List<PayeeRelation> GetDBRelations(PayeeRelationsContext context, int toSkip,
    int bucketSize)
{
    return context.PayeeRelations
        .OrderBy(x => x.id)
        .Include(x => x.PayeeRelation_PayeeVersion)
        .Skip(toSkip)
        .Take(bucketSize)
        .AsNoTracking()
        .ToList();
}

我没有发现您的代码有任何内在错误来指示内存泄漏。我相信您所观察到的只是垃圾收集不会在引用被视为未使用或超出范围时立即完全“释放”内存。

如果内存 use/allocation 是一个问题,那么您应该考虑向下投影到您需要验证的最小可行数据,以便确定需要删除哪些 ID。例如,如果您需要 PayeeRelations 中的 ID 和 Field1,则需要相关 PayeeVersion 中的 Field2 和 Field3:

private class RelationValidationDetails
{
    public long PayeeRelationId { get; set; }
    public string Field1 { get; set; }
    public string Field2 { get; set; }
    public DateTime Field3 { get; set; }
}

...然后在您的查询中:

var validationData = context.PayeeRelations
    .OrderBy(x => x.id)
    .Select(x => new RelationValidationDetails 
    {
        PayeeRelationId = x.Id,
        Field1 = x.Field1,
        Field2 = x.PayeeRelation_PayeeVersion.Field2,
        Field3 = x.PayeeRelation_PayeeVersion.Field3
    }).Skip(toSkip)
    .Take(bucketSize)
    .ToList();

然后您的验证仅使用上述验证详细信息集合来确定需要删除哪些 ID。 (假设它基于字段 1-3 做出此决定)这确保您的查询仅 returns 准确返回最终获取要删除的 ID 所需的数据,从而最大限度地减少内存增长。

可能有人认为,如果稍后需要“Field4”来进行验证,则意味着您必须更新此对象定义并修改查询,而当您只能使用该实体时,这是额外的工作。但是,Field4 可能不会 来自 PayeeRelations 或 PayeeVersion,它可能来自 different 相关的实体,该实体当前未立即加载。这将引入开销,即必须为包装的 GetPayeeRelations 调用的每个调用者添加预先加载另一个 table 的成本,无论他们是否需要该数据。那,或者冒着延迟加载(删除 AsNoTracking())或引入条件复杂性来告诉 GetPayeeRelations 哪些关系需要预先加载的风险。试图预测这种可能性实际上只是 YAGNI 的一个例子。

我通常不建议将 EF 查询隐藏在 getter 方法(例如通用存储库)后面,因为这些往往会在追逐 DNRY 或 SRP 时形成最低公分母。事实上,它们最终成为单点,在许多情况下效率低下,因为如果任何 one 消费者需要一个急切加载的关系,所有消费者都会得到它。通常,让您的消费者能够精确地预测他们需要的东西,而不是担心 similar(而不是相同的)查询可能出现在多个地方,这通常要好得多。