【问题标题】:Hibernate performance issue while inserting massive data插入海量数据时出现休眠性能问题
【发布时间】:2015-03-17 17:26:52
【问题描述】:

我们会将大量数据(单一类型的实体)从 Amazon 的 DynamoDB 迁移到 MySQL 数据库中。我们正在使用 Hibernate 将此类映射到 mysql 实体中。大约有 300 万个实体(不包括列表属性的行)。这是我们的类映射摘要:

@Entity
@Table(name = "CUSTOMER")
public class Customer {
    @Id
    @Column(name = "id")
    private String id;

    //Other properties in which all of them are primitive types/String

    @ElementCollection
    @CollectionTable(name = "CUSTOMER_USER", joinColumns = @JoinColumn(name = "customer_id"))
    @Column(name = "userId")
    private List<String> users;

    // CONSTRUCTORS, GETTERS, SETTERS, etc.
}

users 是一个字符串列表。我们创建了两个 mysql 表,如下所示:

CREATE TABLE CUSTOMER(id VARCHAR(100), PRIMARY KEY(id));
CREATE TABLE CUSTOMER_USER(customer_id VARCHAR(100), userId VARCHAR(100), PRIMARY KEY(customer_id, userId), FOREIGN KEY (customer_id) REFERENCES CUSTOMER(id));

注意:我们不会让 hibernate 生成任何 id 值,我们将我们的 ID 分配给保证唯一的客户实体。

这是我们的 hibernate.cfg.xml:

<hibernate-configuration>    
    <session-factory>    
    <property name="hibernate.dialect">   org.hibernate.dialect.MySQLDialect </property>    
    <property name="hibernate.connection.driver_class"> com.mysql.jdbc.Driver </property>  
    <property name="hibernate.connection.url"> jdbc:mysql://localhost/xxx </property>    
    <property name="hibernate.connection.username"> xxx </property>    
    <property name="hibernate.connection.password"> xxx </property>
    <property name="hibernate.connection.provider_class">org.hibernate.c3p0.internal.C3P0ConnectionProvider</property>
    <property name="hibernate.jdbc.batch_size"> 50 </property>
    <property name="hibernate.cache.use_second_level_cache">false</property>
    <property name="c3p0.min_size">30</property>
    <property name="c3p0.max_size">70</property>
    </session-factory> 
</hibernate-configuration>

我们正在创建一些线程,每个线程都从 Dynamo 读取数据并通过 Hibernate 将它们插入到我们的 MySQl DB 中。以下是每个线程的作用:

// Each single thread brings resultItems from DynamoDB
Session session = factory.openSession();
Transaction tx = session.beginTransaction();
for(int i = 0; i < resultItems.size(); i++) {
    Customer cust = new Customer(resultItems.get(i));
    session.save(cust);
    if(i % BATCH_SIZE == 0) {
        session.flush();
        session.clear();
    }
}
tx.commit();
session.close();

我们有自己的性能监控功能,我们会持续记录整体读/写性能。问题是,迁移从读取/写入 1500 项/秒(平均)开始,但只要 CUSTOMER 和 CUSTOMER_USER 表中的行数增加(几分钟后,r/w 速度约为 500 项/秒)。我对 Hibernate 没有经验,这里是我的问题:

  1. 对于像我们这样的多线程任务,hibernate.cfg.xml 应该是什么样的?我上面给出的内容是否适合这样的任务,还是有什么错误/遗漏的地方?
  2. 恰好有 50 个线程,每个线程执行以下操作:首先从 DynamoDB 读取,然后将结果插入 mysql db,然后从 dynamo 读取,依此类推。因此,与 hibernate 通信的正常运行时间不是 100%。在这种情况下,您建议设置 c3p0 连接池大小的 min_size 和 max_size 什么?为了能够理解这个概念,我是否还应该在 hibernate.cfg.xml 中设置剩余的 c3p0 相关标签?
  3. 如何最大限度地提高批量插入的速度?

注意1我没有写所有属性,因为除了用户列表之外的其余属性都是int,boolean,String等。

注意 2 所有点都经过测试,对性能没有负面影响。当我们不向 mysql db 中插入任何内容时,读取速度会保持稳定数小时。

注意 3 任何关于 mysql 表结构、配置设置、会话/事务、连接池数量、批处理大小等的建议/指导都会非常有帮助!

【问题讨论】:

  • 你能把hibernate在插入一个实体时执行的实际SQL贴出来吗?
  • 如果您对自己的数据完整性有信心,可以尝试在此批量插入期间关闭外键检查,看看这是否有助于提高性能。
  • 您可能需要考虑为每个批次(您刷新/清除的地方)提交/开始您的事务。少量的中等规模的交易通常比少量的大量交易或许多微小的交易要好。
  • 如果您在加载数据时无需进行数据完整性检查就可以逃脱,您可能会删除(或不创建)您的索引(主键、外键),直到加载完所有数据。索引维护增加了一堆开销。
  • 如果您不需要实体上的 Java 或 Hibernate 业务逻辑,移动数据的最快方法是使用数据创建一个文本文件并使用数据库的本机批量加载进行批量加载工具。

标签: java mysql hibernate


【解决方案1】:

假设您在休眠事务中除了将数据插入这两个表之外没有做任何其他事情,您可以使用StatelessSession session = sessionFactory.openStatelessSession(); 而不是普通会话,这减少了维护缓存的开销。但是你将不得不单独保存嵌套的集合对象。 参考https://docs.jboss.org/hibernate/orm/3.3/reference/en/html/batch.html

所以它可能是这样的 -

// Each single thread brings resultItems from DynamoDB
StatelessSession session = factory.openStatelessSession();
Transaction tx = session.beginTransaction();
for(int i = 0; i < resultItems.size(); i++) {
    Customer cust = new Customer(resultItems.get(i));   
    Long id = session.save(cust); // get the generated id
    // TODO: Create a list of related customer users and assign the id to all of them and then save those customer user objects in the same transaction.  
    if(i % BATCH_SIZE == 0) {
        session.flush();
        session.clear();
    }
}
tx.commit();
session.close();

【讨论】:

  • 不应该是if (i + 1 % BATCH_SIZE == 0)
【解决方案2】:

在您的场景中,有 25 个线程同时将数据批量插入到一个表中。 MySQL 必须维护ACID properties,而一张表中许多记录的 25 个事务保持打开或正在提交。这可能会导致巨大的开销。

在从数据库迁移数据时,当与数据库进行多次来回通信时,网络延迟可能会导致严重延迟。在这种情况下,使用多个线程可能是有益的。但是在进行批量提取和批量插入时,数据库驱动程序将(或应该)在不进行太多来回通信的情况下进行数据通信,因此几乎没有什么好处。

在批处理场景中,从读取数据的 1 个线程开始,准备批处理并将其放入队列中,以便 1 个线程从准备好的批处理中写入数据。保持小批量(100 到 1000 条记录)并经常提交(每 100 条记录左右)。这将最大限度地减少维护表的开销。如果网络延迟是一个问题,请尝试使用 2 个线程进行读取和 2 个线程进行写入(但任何性能提升都可能被同时维护 2 个线程使用的表的开销所抵消)。

由于没有生成 ID,您应该受益于休眠配置中已有的 hibernate.jdbc.batch_size 选项。 hibernate.jdbc.fetch_size 选项(将其设置为 250 左右)可能也很有趣。

正如@hermant1900 所提到的,使用StatelessSession 也是一个好主意。但是到目前为止,在 cmets 中@Rob 提到了最快的方法:使用数据库工具将数据导出到文件和import it in MySQL。我很确定这也是首选方法:它需要更少的时间、更少的处理并且涉及的变量更少 - 总体而言更加可靠。

【讨论】:

    猜你喜欢
    • 2017-01-02
    • 1970-01-01
    • 2016-12-06
    • 2011-03-02
    • 1970-01-01
    • 2011-07-11
    • 2021-07-02
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多