Grails,使用withTransaction插入大量数据会导致OutOfMemoryError错误

13

我正在使用Grails 1.1 beta2。我需要将大量数据导入到我的Grails应用程序中。如果我反复实例化一个Grails领域类,然后保存它,性能会非常慢。例如,从电话簿中导入人员:

for (each person in legacy phone book) {
    // Construct new Grails domain class from legacy phone book person
    Person person = new Person(...)
    person.save()
}

这个过程非常缓慢。Grails邮件列表上有人建议在事务中批量保存。所以现在我做了以下更改:

List batch = new ArrayList()
for (each person in legacy phone book) {
    // Construct new Grails domain class from legacy phone book person
    Person person = new Person(...)
    batch.add(person)
    if (batch.size() > 500) {
        Person.withTransaction {
            for (Person p: batch)
                p.save()
            batch.clear()
        }
    }
}
// Save any remaining
for (Person p: batch)
    p.save()

这种方法起初很快,每个事务可以保存500条记录。随着时间的推移,事务变得越来越慢。最开始的几个事务大约需要5秒钟,然后就会逐渐变慢。在大约100个事务之后,每个事务都需要超过一分钟,这仍然是不可接受的。更糟糕的是,最终Grails会耗尽Java堆内存。我可以增加JVM堆大小,但这只是延迟了OutOfMemoryError异常。

有任何原因造成这种情况吗?好像有些内部资源没有被释放。性能变差,内存被占用,然后系统最终会耗尽内存。

根据Grails文档withTransaction将闭包传递给Spring的TransactionStatus对象。我在TransactionStatus中没有找到任何关闭/结束事务的内容。

编辑:我正在从Grails的控制台(grails console)中运行此程序。

编辑:以下是内存溢出异常信息:

Exception thrown: Java heap space

java.lang.OutOfMemoryError: Java heap space
    at org.hibernate.util.IdentityMap.entryArray(IdentityMap.java:194)
    at org.hibernate.util.IdentityMap.concurrentEntries(IdentityMap.java:59)
    at org.hibernate.event.def.AbstractFlushingEventListener.prepareEntityFlushes(AbstractFlushingEventListener.java:113)
    at org.hibernate.event.def.AbstractFlushingEventListener.flushEverythingToExecutions(AbstractFlushingEventListener.java:65)
    at org.hibernate.event.def.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:26)
    at org.hibernate.impl.SessionImpl.flush(SessionImpl.java:1000)
    at org.hibernate.impl.SessionImpl.managedFlush(SessionImpl.java:338)
    at org.hibernate.transaction.JDBCTransaction.commit(JDBCTransaction.java:106)
    at org.springframework.orm.hibernate3.HibernateTransactionManager.doCommit(HibernateTransactionManager.java:655)
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:732)
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:701)
    at org.springframework.transaction.support.TransactionTemplate.execute(TransactionTemplate.java:140)

这是在什么上下文中执行的?石英定时任务?控制器?过去我们使用控制器完成此操作,可以设置一个循环来限制批处理大小,相应地将服务中后续事务的大小限制在一定范围内。 - j pimmel
2个回答

15

Ted Naleid写了一篇关于提高批处理性能的绝佳博客文章。在这里作为参考。


12
这是所有Hibernate应用程序常见的问题,由于Hibernate会话的增长而引起。我猜Grails控制台以类似于“在视图中打开会话”的方式为您保持一个Hibernate会话处于打开状态,这与普通Web请求使用的方法相似。
解决方案是获取当前会话并在每个批处理后清除它。我不确定如何在控制台中使用Spring Bean,通常对于控制器或服务,您只需将它们声明为成员即可。然后,您可以使用sessionFactory.getCurrentSession()获取当前会话。为了清除它,只需调用session.clear(),或者如果您想有选择性地使用,则可以为每个Person对象使用session.evict(Object)
对于控制器/服务:
class FooController {
    def sessionFactory

    def doStuff = {
        List batch = new ArrayList()
        for (each person in legacy phone book) {
            // Construct new Grails domain class from legacy phone book person
            Person person = new Person(...)
            batch.add(person)
            if (batch.size() > 500) {
                Person.withTransaction {
                    for (Person p: batch)
                        p.save()
                    batch.clear()
                }
                // clear session here.
                sessionFactory.getCurrentSession().clear();
            }
        }
        // Save any remaining
        for (Person p: batch)
            p.save()
        }
    }
}
希望这能帮到你。

我会改进那段代码,使用session.clear()在循环的第N次迭代中清除,而不是每次迭代都清除。 - Pavel Vlasov
完全同意...现在看那段代码,我都不确定它是否能正常工作。 - Gareth Davis

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