创建大量对象时出现内存不足错误 C#

15

我的应用程序正在处理从 MySQL 数据库检索的 100 万条记录。为了这样做,我使用 Linq 获取记录并使用 .Skip() 和 .Take() 每次处理 250 条记录。对于每个检索到的记录,我需要创建 0 到 4 个项目,并将它们添加到数据库中。因此,平均需要创建的总项目数量约为 200 万。

IQueryable<Object> objectCollection = dataContext.Repository<Object>();
int amountToSkip = 0;
IList<Object> objects = objectCollection.Skip(amountToSkip).Take(250).ToList();
while (objects.Count != 0)
        {
            using (dataContext = new LinqToSqlContext(new DataContext()))
            {
                foreach (Object objectRecord in objects)
                {
                    // Create 0 - 4 Random Items
                    for (int i = 0; i < Random.Next(0, 4); i++)
                    {
                        Item item = new Item();
                        item.Id = Guid.NewGuid();
                        item.Object = objectRecord.Id;
                        item.Created = DateTime.Now;
                        item.Changed = DateTime.Now;
                        dataContext.InsertOnSubmit(item);
                    }
                }
                dataContext.SubmitChanges();
            }
            amountToSkip += 250;
            objects = objectCollection.Skip(amountToSkip).Take(250).ToList();
        }

现在问题出在创建项目时。当运行应用程序(甚至没有使用dataContext)时,内存持续增加。就好像这些项目永远不会被处理掉一样。是否有人注意到我做错了什么?

提前致谢!


IQueryable<Object> objectCollection = dataContext.Repository<Object>(); 可查询的对象集合 = dataContext.Repository<Object>(); - Bas
为什么您要批量处理250个对象,而不是直接迭代objectCollection? - Jens
4
实际上,他对于拉取数据的方法很好。只是迭代操作也包括以块的形式拉取数据,而其分页方式由Linq实现自行决定。此外,你可以获得每次运行只需更新250个项目的好处,而不是更新任意数量的项目并依赖框架来正确地批处理数据。 - Tigraine
你有没有机会在代码上运行dotTrace或其他分析器?这样可以很快地显示出问题所在。 - Tigraine
你尝试过对插入进行事务处理吗? - Sorin Comanescu
显示剩余3条评论
5个回答

7

好的,我刚刚和我的一位同事讨论了这种情况,我们得出了以下解决方案,它是有效的!

int amountToSkip = 0;
var finished = false;
while (!finished)
{
      using (var dataContext = new LinqToSqlContext(new DataContext()))
      {
           var objects = dataContext.Repository<Object>().Skip(amountToSkip).Take(250).ToList();
           if (objects.Count == 0)
                finished = true;
           else
           {
                foreach (Object object in objects)
                {
                    // Create 0 - 4 Random Items
                    for (int i = 0; i < Random.Next(0, 4); i++)
                    {
                        Item item = new Item();
                        item.Id = Guid.NewGuid();
                        item.Object = object.Id;
                        item.Created = DateTime.Now;
                        item.Changed = DateTime.Now;
                        dataContext.InsertOnSubmit(item);
                     }
                 }
                 dataContext.SubmitChanges();
            }
            // Cumulate amountToSkip with processAmount so we don't go over the same Items again
            amountToSkip += processAmount;
        }
}

使用这种实现方式,我们会在每次使用Skip()和Take()缓存时都清除缓存,从而避免了内存泄漏!

6
啊,好老的InsertOnSubmit内存泄漏问题。我在使用LINQ to SQL从大型CSV文件中加载数据时遇到过这个问题,试图解决它让我头痛不已。问题在于即使调用了SubmitChangesDataContext仍然会继续跟踪使用InsertOnSubmit添加的所有对象。解决方法是在一定数量的对象之后调用SubmitChanges,然后为下一个批次创建一个新的DataContext。当旧的DataContext被垃圾回收时,它所跟踪的所有插入对象(你不再需要的)也将被回收。
“但等等!”你说,“创建和处理多个DataContext会有巨大的开销!”好吧,如果你创建一个单一的数据库连接并将其传递给每个DataContext构造函数,那么就不会有很大的开销了。这样,数据库与应用程序之间只保持一个连接,而DataContext对象则是表示小工作单元的轻量级对象,在完成任务后应该被丢弃(例如提交一定数量的记录)。

我在问题中已经说过,即使我没有使用DataContext,我仍然会出现这个泄漏问题,所以它与InsertOnSubmit或SubmitChanges无关(我已经测试过了)。最好的做法是在using块中使用DataContext。DataContextes是轻量级的,意味着要经常重新创建(参见:https://dev59.com/WnVD5IYBdhLWcg3wAGiD)。我已经尝试过使用一个DataContext来执行所有事务,但情况更糟。 - Bas
你正在重复我说的话 - 每个小工作单元都要使用一个新的DataContext(当然,在using语句中)。你又是如何在没有DataContext的情况下测试你的示例的呢?你从哪里获取了“objects”集合? - Allon Guralnek
抱歉,我的意思是不包括InsertOnSubmit和SubmitChanges调用;] 我的失误。起初我也认为InsertOnSubmit和SubmitChanges是问题所在,但解决了这个问题并进行了第二次运行后,仍然存在泄漏。泄漏是由于Skip和Take缓存了所有检索到的项目,并在运行时从未自动处置它。因此,最终我在一个缓存列表中有200万个项目。 - Bas
1
是的,那正是我在答案中描述的确切问题。顺便说一句,在我的情况下,即使解决了内存泄漏问题,它仍然不够高效,所以我编写了一个用C#编写的CLR存储过程,运行速度快了约200倍(加载730万条记录只需要三分钟,而不是十个小时)。 - Allon Guralnek
哇,好的,听起来很棒^^我已经解决了内存问题,但我必须同意你的看法,性能并不是真正值得欢呼的东西。很不错,你可以通过编写存储过程来解决这个问题。 - Bas

2

我猜测可能是IQueryable导致了内存泄漏。也许MySQL没有适当的Take/Skip方法实现,而是在内存中进行分页?奇怪的事情总会发生,但你的循环看起来没问题。所有引用应该超出范围并被垃圾回收。


0

你尝试过像这样在循环外部声明Item吗:

IQueryable<Object> objectCollection = dataContext.Repository<Object>();
int amountToSkip = 0;
IList<Object> objects = objectCollection.Skip(amountToSkip).Take(250).ToList();
Item item = null;
while (objects.Count != 0)
        {
            using (dataContext = new LinqToSqlContext(new DataContext()))
            {
                foreach (Object objectRecord in objects)
                {
                    // Create 0 - 4 Random Items
                    for (int i = 0; i < Random.Next(0, 4); i++)
                    {
                        item = new Item();
                        item.Id = Guid.NewGuid();
                        item.Object = objectRecord.Id;
                        item.Created = DateTime.Now;
                        item.Changed = DateTime.Now;
                        dataContext.InsertOnSubmit(item);
                    }
                }
                dataContext.SubmitChanges();
            }
            amountToSkip += 250;
            objects = objectCollection.Skip(amountToSkip).Take(250).ToList();
        }

尝试过这个,但没成功 >.< 我认为,就像Tigraine所说的那样,这是因为IQueryable和Take/Skip的原因... - Bas

0

没错。

所以在那个循环的结尾,你将尝试在列表中有200万个项目,对吧? 在我看来,答案非常简单:存储更少的项目或获取更多的内存。

-- 编辑:

有可能我读错了,我可能需要编译和测试它,但我现在不能这样做。 我会把它放在这里,但我可能是错的,我还没有仔细审查它,不过这个回答可能会证明有用,也可能不会。(根据投票结果判断,我认为它不会 :P)


不,他有一个包含200万条目的数据库,每次取出250个条目,然后将4个新的子条目添加到数据库中。在内存中没有任何列表。请仔细阅读问题。 - Tigraine
@Tigraine 我认为是跳过的部分出了问题。这只是我的猜测。 - Noon Silk
缓存在对象集合列表objectCollection中是瓶颈。它不会自动释放。 - Bas

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