实体框架大数据集,内存溢出异常

35

我正在处理一个非常大的数据集,大约有200万条记录。我有以下代码,但是在处理了大约三个批次(约60万条记录)后,会出现内存不足异常。我理解每当它遍历每个批次实体框架时,都会懒加载,然后试图将完整的200万条记录构建到内存中。有没有办法在处理完一个批次后卸载它呢?

ModelContext dbContext = new ModelContext();
IEnumerable<IEnumerable<Town>> towns = dbContext.Towns.OrderBy(t => t.TownID).Batch(200000);
foreach (var batch in towns)
{
    SearchClient.Instance.IndexMany(batch, SearchClient.Instance.Settings.DefaultIndex, "Town", new SimpleBulkParameters() { Refresh = false });
}

注意:批处理方法来自于此项目:https://code.google.com/p/morelinq/ 搜索客户端是:https://github.com/Mpdreamz/NEST

大量的数据是一个场景,我不确定ORM是否是一个合适的工具... - Vadim
@Vadim,ORM是一个适当的工具,可以处理业务逻辑,而无需担心底层数据存储,但使用ORM编写批处理还有更简单的方法。 - Akash Kava
2
@AkashKava,当然是这样的。问题在于,当你将ORM与大量数据结合使用时,你总会发现自己“取消”了所有种类的ORM方面,而在其他情况下,你会以特定的方式编写代码来解决你正在使用的ORM的问题。我想说的是 - 在处理大量数据时,ORM变得很棘手。 - Vadim
@Vadim,它变得有问题只是因为它被错误地使用,并不意味着它不应该被使用。大量的数据不应该一次性在内存中处理,而是应该采用正确的分批处理方式。请看我的回答,我们正在使用ORM每天处理数百万条记录。大型操作必须分解成一系列较小的步骤。 - Akash Kava
2
批处理与 Skip(batchNo*batchSize).Take(batchSize) 有何不同? - B2K
2个回答

82
问题在于,当您从EF获取数据时,实际上会创建两份数据副本,一份返回给用户,另一份由EF保留并用于更改检测(以便它可以将更改持久化到数据库)。EF将此第二组保存为上下文的生命周期,并且正是这个集合导致了内存耗尽。
您有两个选项来处理这个问题。
  1. renew your context each batch
  2. Use .AsNoTracking() in your query eg:

    IEnumerable<IEnumerable<Town>> towns = dbContext.Towns.AsNoTracking().OrderBy(t => t.TownID).Batch(200000);
    

这告诉EF不要保留用于更改检测的副本。你可以在我的博客上阅读有关AsNoTracking功能以及其对性能影响的更多信息:http://blog.staticvoid.co.nz/2012/4/2/entity_framework_and_asnotracking


NoTracking会保留导航属性吗?例如,如果我想设置/添加相关对象,它会保存吗? - Akash Kava
2
@AkashKava 不会的,如果你想保存任何东西,你需要先将它附加回上下文,如果实体具有导航属性,这些属性也需要被附加。如果你真的想修改加载的实体,我建议使用第一种方法。 - undefined
2
谢谢@LukeMcGregor,我发现在每个批次上更新上下文效果非常好。 - Mike Norgate
1
你如何更新你的上下文?只需要 dbContext = new DbContext() 吗? - muttley91
1
@mrmashal 一个在IEnumerable<T>上的扩展方法,它返回一个IEnumerable<IEnumerable<T>>,其中内部集合有20000个项。我自己写的,但如果你想要类似的东西,它非常简单。 - undefined
显示剩余5条评论

-1

我编写了一个迁移例程,从一个数据库中读取数据并将其(在布局上进行轻微更改)写入到另一个不同类型的数据库中。在这种情况下,为每个批处理续订连接并使用AsNoTracking()无法满足我的要求。

请注意,使用JET的'97版本会出现此问题。在其他数据库中可能可以完美解决。

然而,以下算法确实解决了内存不足的问题:

  • 使用一个连接进行读取,另一个连接进行写入/更新
  • 使用AsNoTracking()进行读取
  • 每写入/更新约50行数据,检查内存使用情况,根据需要恢复内存并重置输出DB上下文(以及连接的表):

    var before = System.Diagnostics.Process.GetCurrentProcess().VirtualMemorySize64;
    if (before > 800000000)
    {
        dbcontextOut.SaveChanges();
        dbcontextOut.Dispose();
        GC.Collect();
        GC.WaitForPendingFinalizers();
        dbcontextOut = dbcontextOutFunc();
        tableOut = Dynamic.InvokeGet(dbcontextOut, outputTableName);
    }
    

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接