JPA与HIBERNATE插入速度非常慢

3
我正在使用JAP和HIBERNATE将一些数据插入到SQL Server 2008 R2中。除了速度非常慢之外,一切都“正常”。插入20000行需要大约45秒,而C#脚本只需要不到1秒钟。
这个领域的任何老手都能提供一些帮助吗?我会非常感激。
更新:从下面的答案中得到了一些很好的建议,但仍然没有达到预期的效果。速度还是一样慢。
以下是更新后的persistence.xml:
<persistence version="2.0"
xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
<persistence-unit name="ClusterPersist"
    transaction-type="RESOURCE_LOCAL">
    <provider>org.hibernate.ejb.HibernatePersistence</provider>
    <class>cluster.data.persist.sqlserver.EventResult</class>
    <exclude-unlisted-classes>true</exclude-unlisted-classes>
    <properties>
        <property name="javax.persistence.jdbc.url"
            value="jdbc:sqlserver://MYSERVER:1433;databaseName=MYTABLE" />
        <property name="javax.persistence.jdbc.user" value="USER" />
        <property name="javax.persistence.jdbc.password" value="PASSWORD" />
        <property name="javax.persistence.jdbc.driver"
            value="com.microsoft.sqlserver.jdbc.SQLServerDriver" />
        <property name="hibernate.show_sql" value="flase" />
        <property name="hibernate.hbm2ddl.auto" value="update" />

        <property name="hibernate.connection.provider_class"
            value="org.hibernate.service.jdbc.connections.internal.C3P0ConnectionProvider" />

        <property name="hibernate.c3p0.max_size" value="100" />
        <property name="hibernate.c3p0.min_size" value="0" />
        <property name="hibernate.c3p0.acquire_increment" value="1" />
        <property name="hibernate.c3p0.idle_test_period" value="300" />
        <property name="hibernate.c3p0.max_statements" value="0" />
        <property name="hibernate.c3p0.timeout" value="100" />
        <property name="hibernate.jdbc.batch_size" value="50" />
        <property name="hibernate.cache.use_second_level_cache" value="false" />
    </properties>
</persistence-unit>

以下是更新后的代码部分:

public static void writeToDB(String filePath) throws IOException {

    EntityManager entityManager = entityManagerFactory.createEntityManager();
    Session session = (Session) entityManager.getDelegate();
    Transaction tx = session.beginTransaction();
    int i = 0;

    URL filePathUrl = null;
    try {
        filePathUrl = new URL(filePath);
    } catch (MalformedURLException e) {
        filePathUrl = (new File(filePath)).toURI().toURL();
    }

    String line = null;
    BufferedReader stream = null;

    try {
        InputStream in = filePathUrl.openStream();
        stream = new BufferedReader(new InputStreamReader(in));


        // Read each line in the file
        MyRow myRow = new MyRow();
        while ((line = stream.readLine()) != null) {
            String[] splitted = line.split(",");
            int num1 = Integer.valueOf(splitted[1]);
            float num2= Float.valueOf(splitted[6]).intValue();

            myRow.setNum1(num1);
            myRow.setNum2(num2);

            session.save(myRow);

            if (i % 50 == 0) { 
                session.flush();
                session.clear();
            }

            i++;

        }
        tx.commit();

    } finally {
        if (stream != null)
            stream.close();
    }
    session.close();

}

更新:以下是MyRow的源代码:

@Entity
@Table(name="MYTABLE")
public class MyRow {    

@Id
@GeneratedValue(strategy=GenerationType.IDENTITY) 
private Long id;

@Basic
@Column(name = "Num1")
private int Num1;

@Basic
@Column(name = "Num2")
private float Num2;

public Long getId() {
    return id;
}

public void setId(Long id) {
    this.id = id;
}

public float getNum1() {
    return Num1;
}

public void setNum1(float num1) {
    Num1 = num1;
}

public int getNum2() {
    return Num2;
}

public void setNum2(int num2) {
    Num2 = num2;
}
}
4个回答

7

要启用JDBC批处理,您应将属性hibernate.jdbc.batch_size初始化为介于10和50之间的整数。

hibernate.jdbc.batch_size=50

如果速度仍然不如预期,那么我会仔细查看上面的文档,特别关注注释和4.1节。特别是注意事项中提到的:“如果您使用标识符生成器,则Hibernate会在JDBC级别自动禁用插入批处理。”

你是否正在尝试使用匿名事务句柄?请将它们存储在变量中。你能否编辑你的问题以指示最新的代码? - Elliott Frisch
我已经看到了文件读取部分。它是本地的,所以从读取文件的速度非常快。并且我已经包含了MyRow源代码。非常感谢。 - Yellow Duck
没错,我按照它的指示添加了 '<property name="hibernate.cache.use_second_level_cache" value="false" />',但似乎没有任何区别。或者,我理解错了? - Yellow Duck
我相信你误解了。你需要使用另一个身份管理器来进行批处理。 - Elliott Frisch
1
我将身份管理器从标识生成器改为手动方式。这样速度快了很多。顺便问一下,有没有一个可以与批量插入一起使用的好的标识生成器?请同时更新你的答案,这样人们就不必阅读评论来理解正在发生的事情了。 - Yellow Duck
显示剩余4条评论

7

问题

如果你使用Hibernate作为ORM,其中一个主要的性能问题是其“脏检查”的实现方式(因为没有字节码增强,而这是所有基于JDO的ORM和一些其他ORM中的标准,脏检查始终是一种低效的hack)。

在刷新时,需要对会话中的每个对象进行脏检查,以查看它是否是“脏的”,即自从从数据库加载以来,其属性是否发生了更改。对于所有“脏”(已更改)的对象,Hibernate必须生成SQL更新以更新表示脏对象的记录。

在内存中执行与从数据库加载对象时拍摄的快照之间的“逐字段”比较,Hibernate脏检查在除少量对象外通常非常缓慢。例如,当HTTP请求加载多个对象以显示页面时,需要进行更多的脏检查才能提交。

Hibernate脏检查机制的技术细节

您可以在此处阅读有关Hibernate脏检查机制的“逐字段”比较的详细信息:

Hibernate如何检测实体对象的脏状态?

其他ORM中如何解决这个问题

其他一些ORM使用的更有效机制是使用自动生成的“脏标志”属性,而不是“逐字段”比较。 但是,这通常仅在使用和推广字节码增强或字节码“编织”的ORM(通常是基于JDO的ORM)中才可用,例如http://datanucleus.org和其他ORM。

在通过DataNucleus或任何支持此功能的其他ORM进行字节码增强期间,每个实体类都会被增强以:

  • 添加一个隐式脏标志属性
  • 在类中的每个setter方法中添加代码,以便在调用时自动设置脏标志

然后,在刷新时,只需要检查脏标志,而不是执行逐字段比较-可以想象,这快得多。

“逐字段”脏检查的其他负面影响

Hibernate脏检查的另一个低效之处是需要在内存中保留每个加载的对象的快照,以避免在脏检查期间重新加载并针对数据库进行检查。

每个对象快照都是其所有字段的集合。

除了Hibernate脏检查机制在刷新时的性能问题外,该机制还会增加应用程序的额外内存消耗和CPU使用量,这是实例化并初始化从数据库加载的每个单独对象的快照所需要的,这可能会达到数千或数百万。

Hibernate已经引入了字节码增强技术来解决这个问题,但我曾经参与过许多ORM持久化项目(包括Hibernate和非Hibernate),但我从未见过使用该功能的Hibernate持久化项目,可能是由于以下原因之一:
  • Hibernate传统上推广其“无需字节码增强”的特点,当人们评估ORM技术时
  • 历史上Hibernate的字节码增强实现存在可靠性问题,可能不像那些从一开始就使用和推广字节码增强的ORM这样成熟
  • 一些人仍然害怕使用字节码增强,这是由于在ORM早期,某些团体灌输人们反对“字节码增强”的立场和恐惧。

现在,字节码增强技术被用于许多不同的事情,而不仅仅是持久化。它已经几乎变成了主流。


6

虽然这是一个老生常谈的话题,但今天我在寻找其他内容时偶然发现了它。我必须发帖讨论这个常见但不幸被理解和记录得不够好的问题。Hibernate的文档长期以来只有上述简短的说明。

从版本5开始,有一个更好但仍然很少的解释:

https://docs.jboss.org/hibernate/orm/5.3/userguide/html_single/Hibernate_User_Guide.html#identifiers-generators-identity

非常大集合的缓慢插入问题只是选择了错误的Id生成策略所导致的。

@Id
@GeneratedValue(strategy=GenerationType.IDENTITY) 

当使用Identity策略时,需要了解的是数据库服务器在物理插入时创建行的标识。Hibernate需要知道分配的ID以使对象处于持久化状态,在会话中。数据库生成的ID仅在插入响应中才知道。Hibernate别无选择,只能执行20000个单独的插入操作才能检索生成的ID。就我所知,它不能与批处理一起使用,无论是Sybase还是MSSQL。这就是为什么,无论你多努力并且所有批处理属性都正确配置,Hibernate都将执行单独的插入操作。
我所知道并经常使用的唯一解决方案是选择客户端ID生成策略,而不是流行的数据库端Identity策略。 我经常使用:
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
@GenericGenerator(strategy = "org.hibernate.id.enhanced.SequenceStyleGenerator")

需要进行一些配置才能让它正常工作,但这就是它的本质。当使用客户端Id生成时,Hibernate会在访问数据库之前设置所有20000个对象的Id。并且通过先前答案中所见到的适当的批处理属性,Hibernate将按批次进行插入操作,如预期。

不幸的是,Identity生成器非常方便和流行,它似乎无处不在,在所有示例中都没有明确解释使用此策略的后果。我读过许多所谓的“高级”Hibernate书籍,但从未看到有一个解释Identity对大型数据集上底层插入性能的影响。


2

1
非常感谢。你能否给出一个关于如何设置persistence.xml的简单示例?另一个更复杂的层次是我使用了JPA,因此一些设置可能无法轻松地转换到persistence.xml。 - Yellow Duck
提供的链接中指出,在批处理之前,请启用JDBC批处理。要启用JDBC批处理,请将属性hibernate.jdbc.batch_size设置为介于10和50之间的整数值。 - SJuan76
刚试了一下,速度还是一样。这是我在persistence.xml中添加的内容 "<property name="hibernate.jdbc.batch_size" value="50" />"。然后我根据链接中的示例相应地修改了代码。代码看起来像这样:'Session session = (Session) entityManager.getDelegate(); Transaction tx = session.beginTransaction();' - Yellow Duck
这个有效。刷新然后清除会提供稳定且线性的性能。不过我不确定后果是什么。就我所见,batch_size为50没有任何影响。 - mjs

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