OutOfMemoryException:在任务中使用DbContext

3
以下是我目前处理的设置:
  • 我有一个包含大约1,000,000条记录的SQL表需要更新
  • 更新在单独的线程中进行(由任务启动)
  • 由于线程内存有限,我将列表分批处理,每批1000个条目 (在主线程/没有任何任务的测试项目中运行时,不会出现OOM异常)
  • UpdateList()函数可以更新列表字段或为此或其他表创建新记录
  • Process_Failure()函数中,我为整个列表使用单个上下文实例
  • Process_Success()函数中,我将while循环移到上下文之外
private void Process_Success()
{
    var totalProcessedCounter = 0;

    while( true )
    {
        using( var context = new MyDbContext() )
        {
            var list = context.MyClass.OrderBy( x => x.Id )
                .Skip( totalProcessedCounter ).Take( 1000 )
                .ToList();

            if( !list.Any() )
                break;

            UpdateList( list );

            totalProcessedCounter += list.Count;
        }
    }
}

private void Process_Failure()
{
    var totalProcessedCounter = 0;

    using( var context = new MyDbContext() )
    {
        while( true )
        {
            var list = context.MyClass.OrderBy( x => x.Id )
                .Skip( totalProcessedCounter ).Take( 1000 )
                .ToList(); // throws OutOfMemoryException

            if( !list.Any() )
                break;

            UpdateList( list );

            totalProcessedCounter += list.Count;
        }
    }
}

private void UpdateList( List<MyClass> list )
{
    var doSaveChanges = false;

    list = list.Where( x => SomeFilter( x ) ).ToList();

    using( var context = new MyDbContext() )
    {
        foreach( var item in list )
        {
            ChangeItem( item );
        }

        if( doSaveChanges )
            context.SaveChanges();
    }
}

当我在UpdateList()的嵌套函数调用中创建另一个实例时,是否会以某种方式污染/填充context


在线程中,栈空间是有限的,但堆内存则不是。 - Matthew Watson
2
你实际上不需要执行 .ToList()。这会执行查询并将所有东西倾倒到内存中。移除它,.Any() 将在没有它的情况下正常工作。 - Mike Eason
1
@user5997884 如果您删除 .ToList() 并跳过该行,将光标悬停在 list 变量上,您应该会在展开器中看到一个 结果视图。这将执行查询,您将能够查看结果。或者您可以使用 立即窗口 并在那里执行查询:list.ToList() 应该打印出结果。 - Mike Eason
1
@MikeEason 非常感谢!我以前从没见过这个。我想现在得大量重构代码了... :) - user5997884
然而,我仍然不明白oom异常。GC难道不会删除从上下文中加载的先前列表吗?或者EF会将所有加载的记录保留在内存中直到上下文被释放吗? - user5997884
显示剩余4条评论
1个回答

2
你出现异常的原因是因为DbContext缓存了从数据库读取的数据,因此如果你继续向其缓存中添加实体,最终会耗尽内存并导致OutOfMemoryException。实体不会被垃圾回收程序清除,因为它们被DbContext引用。

尝试使用.AsNoTracking()

private void Process_NoTracking()
{
    var totalProcessedCounter = 0;

    using( var context = new MyDbContext() )
    {
        while( true )
        {
            var list = context
                          .MyClass
                          .AsNoTracking()
                          .OrderBy( x => x.Id )
                          .Skip( totalProcessedCounter )
                          .Take( 1000 )
                          .ToList(); 

            if( !list.Any() )
                break;

            UpdateList( list );

            totalProcessedCounter += list.Count;
        }
    }
}

但是,如果您不跟踪这些实体,更新它们会更加困难(即“将现有实体附加到上下文”),因为这些实体不属于任何上下文并且没有被跟踪。
我不会使用EF来处理这样的事情,这看起来更适合使用UPDATE/SELECT SQL语句。

如果你对每个更新批次都使用一个新的上下文(context),那么你就可以做到这一点。 - Dave Van den Eynde
当然,但似乎原帖的作者已经得出了这个结论,因为这就是“Process_Success”所做的。 - vtortola

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