使用Hibernate在5秒内向MySQL中插入100000行数据。

26
我试图使用Hibernate(JPA)在5秒内向MYSQL表中插入100,000行。我尝试了Hibernate提供的所有技巧,但仍然无法做到比35秒更好。
第一个优化:我从IDENTITY序列生成器开始,结果需要60秒来插入。后来我放弃了序列生成器,开始通过读取MAX(id),并使用AtomicInteger.incrementAndGet()来自己分配@Id字段。这将插入时间减少到35秒。
第二个优化:我启用了批量插入,通过添加以下配置: 30 true thread true 我惊讶地发现批量插入完全没有减少插入时间。它仍然是35秒!
现在,我考虑尝试使用多个线程进行插入。任何人有任何指针吗?我应该选择MongoDB吗?
下面是我的配置: 1. Hibernate配置`
<bean id="entityManagerFactoryBean" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="packagesToScan" value="com.progresssoft.manishkr" />
        <property name="jpaVendorAdapter">
            <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter" />
        </property>
        <property name="jpaProperties">
            <props>
                <prop key="hibernate.hbm2ddl.auto">${hibernate.hbm2ddl.auto}</prop>
                <prop key="hibernate.dialect">${hibernate.dialect}</prop>
                <prop key="hibernate.show_sql">${hibernate.show_sql}</prop>
                <prop key="hibernate.format_sql">${hibernate.format_sql}</prop>
                <prop key="hibernate.jdbc.batch_size">30</prop>
                <prop key="hibernate.order_inserts">true</prop>
                <prop key="hibernate.current_session_context_class">thread</prop>
                <prop key="hibernate.jdbc.batch_versioned_data">true</prop>
            </props>
        </property>
    </bean>

    <bean class="org.springframework.jdbc.datasource.DriverManagerDataSource"
          id="dataSource">
        <property name="driverClassName" value="${database.driver}"></property>
        <property name="url" value="${database.url}"></property>
        <property name="username" value="${database.username}"></property>
        <property name="password" value="${database.password}"></property>
    </bean>

    <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
        <property name="entityManagerFactory" ref="entityManagerFactoryBean" />
    </bean>



    <tx:annotation-driven transaction-manager="transactionManager" />
  1. 实体配置:

`

@Entity
@Table(name = "myEntity")
public class MyEntity {

    @Id
    private Integer id;

    @Column(name = "deal_id")
    private String dealId;

    ....
    ....

    @Temporal(TemporalType.TIMESTAMP)
    @Column(name = "timestamp")
    private Date timestamp;

    @Column(name = "amount")
    private BigDecimal amount;

    @OneToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "source_file")
    private MyFile sourceFile;

    public Deal(Integer id,String dealId, ....., Timestamp timestamp, BigDecimal amount, SourceFile sourceFile) {
        this.id = id;
        this.dealId = dealId;
        ...
        ...
        ...
        this.amount = amount;
        this.sourceFile = sourceFile;
    }


    public String getDealId() {
        return dealId;
    }

    public void setDealId(String dealId) {
        this.dealId = dealId;
    }

   ...

   ...


    ....

    public BigDecimal getAmount() {
        return amount;
    }

    public void setAmount(BigDecimal amount) {
        this.amount = amount;
    }

    ....


    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }
  1. 代码持久化(服务):

`

@Service
@Transactional
public class ServiceImpl implements MyService{

    @Autowired
    private MyDao dao;
....

`void foo(){
        for(MyObject d : listOfObjects_100000){
            dao.persist(d);
        }
}

4. Dao 类:

`

@Repository
public class DaoImpl implements MyDao{

    @PersistenceContext
    private EntityManager em;

    public void persist(Deal deal){
        em.persist(deal);
    }
}

日志: `

DEBUG o.h.e.j.b.internal.AbstractBatchImpl - Reusing batch statement
18:26:32.906 [http-nio-8080-exec-2] DEBUG org.hibernate.SQL - insert into deal (amount, deal_id, timestamp, from_currency, source_file, to_currency, id) values (?, ?, ?, ?, ?, ?, ?)
18:26:32.906 [http-nio-8080-exec-2] DEBUG o.h.e.j.b.internal.AbstractBatchImpl - Reusing batch statement
18:26:32.906 [http-nio-8080-exec-2] DEBUG org.hibernate.SQL - insert into deal (amount, deal_id, timestamp, from_currency, source_file, to_currency, id) values (?, ?, ?, ?, ?, ?, ?)
18:26:32.906 [http-nio-8080-exec-2] DEBUG o.h.e.j.b.internal.AbstractBatchImpl - Reusing batch statement
18:26:32.906 [http-nio-8080-exec-2] DEBUG org.hibernate.SQL - insert into deal (amount, deal_id, timestamp, from_currency, source_file, to_currency, id) values (?, ?, ?, ?, ?, ?, ?)
18:26:32.906 [http-nio-8080-exec-2] DEBUG o.h.e.j.b.internal.AbstractBatchImpl - Reusing batch statement
18:26:32.906 [http-nio-8080-exec-2] DEBUG org.hibernate.SQL - insert into deal (amount, deal_id, timestamp, from_currency, source_file, to_currency, id) values (?, ?, ?, ?, ?, ?, ?)
18:26:32.906 [http-nio-8080-exec-2] DEBUG o.h.e.j.b.internal.AbstractBatchImpl - Reusing batch statement
18:26:32.906 [http-nio-8080-exec-2] DEBUG org.hibernate.SQL - insert into deal (amount, deal_id, timestamp, from_currency, source_file, to_currency, id) values (?, ?, ?, ?, ?, ?, ?)
18:26:32.906 [http-nio-8080-exec-2] 
抱歉,我只能以英文回答问题。
DEBUG o.h.e.j.b.internal.AbstractBatchImpl - Reusing batch statement
18:26:34.002 [http-nio-8080-exec-2] DEBUG org.hibernate.SQL - insert into deal (amount, deal_id, timestamp, from_currency, source_file, to_currency, id) values (?, ?, ?, ?, ?, ?, ?)
18:26:34.002 [http-nio-8080-exec-2] DEBUG o.h.e.j.b.internal.AbstractBatchImpl - Reusing batch statement
18:26:34.002 [http-nio-8080-exec-2] DEBUG org.hibernate.SQL - insert into deal (amount, deal_id, timestamp, from_currency, source_file, to_currency, id) values (?, ?, ?, ?, ?, ?, ?)
18:26:34.002 [http-nio-8080-exec-2] DEBUG o.h.e.j.b.internal.AbstractBatchImpl - Reusing batch statement
18:26:34.002 [http-nio-8080-exec-2] DEBUG org.hibernate.SQL - insert into deal (amount, deal_id, timestamp, from_currency, source_file, to_currency, id) values (?, ?, ?, ?, ?, ?, ?)
18:26:34.002 [http-nio-8080-exec-2] DEBUG o.h.e.j.b.internal.AbstractBatchImpl - Reusing batch statement
18:26:34.002 [http-nio-8080-exec-2] DEBUG org.hibernate.SQL - insert into deal (amount, deal_id, timestamp, from_currency, source_file, to_currency, id) values (?, ?, ?, ?, ?, ?, ?)
18:26:34.002 [http-nio-8080-exec-2] DEBUG o.h.e.j.batch.internal.BatchingBatch - Executing batch size: 27
18:26:34.011 [http-nio-8080-exec-2] DEBUG org.hibernate.SQL - update deal_source_file set invalid_rows=?, source_file=?, valid_rows=? where id=?
18:26:34.015 [http-nio-8080-exec-2] DEBUG o.h.e.j.batch.internal.BatchingBatch - Executing batch size: 1
18:26:34.018 [http-nio-8080-exec-2] DEBUG o.h.e.t.i.jdbc.JdbcTransaction - committed JDBC Connection
18:26:34.018 [http-nio-8080-exec-2] DEBUG o.h.e.t.i.jdbc.JdbcTransaction - re-enabling autocommit
18:26:34.032 [http-nio-8080-exec-2] DEBUG o.s.orm.jpa.JpaTransactionManager - Closing JPA EntityManager [org.hibernate.jpa.internal.EntityManagerImpl@2354fb09] after transaction
18:26:34.032 [http-nio-8080-exec-2] DEBUG o.s.o.jpa.EntityManagerFactoryUtils - Closing JPA EntityManager
18:26:34.032 [http-nio-8080-exec-2] DEBUG o.h.e.j.internal.JdbcCoordinatorImpl - HHH000420: Closing un-released batch
18:26:34.032 [http-nio-8080-exec-2] DEBUG o.h.e.j.i.LogicalConnectionImpl - Releasing JDBC connection
18:26:34.033 [http-nio-8080-exec-2] DEBUG o.h.e.j.i.LogicalConnectionImpl - Released JDBC connection

'


@gati sahu 我配置的批处理大小是30,但我在日志中看到“执行批处理大小:27”。而且我只看到一次“执行批处理大小:27”。请查看我已更新的日志。另一个问题是,我已经了解到即使使用多线程,JDBC驱动程序的插入也在synchronized()方法中,因此实际的插入仍然是逐个进行的? - Kumar Manish
@JacobBelanger 网络会如何成为问题?这是一个本地主机环境,有本地的Tomcat和MySQL在运行! - Kumar Manish
1
确保您知道瓶颈在哪里。插入语句应该只在提交时执行,那么这35秒钟中有多少时间用于处理数据库业务,有多少时间只是Hibernate开销?堆使用情况如何?定期flush() em是否有帮助? - piet.t
我不确定batch_insert是否正在进行!如果我查看日志,似乎插入仍然是一个接一个地进行的?此外,我尝试将batch_size增加到500/1000。 500的值让我在27秒内插入,而1000的值让我在29秒内插入。 - Kumar Manish
3
  1. 不要使用DriverManagerDataSource,应该使用适当的连接池。
  2. 不要使用Spring进行事务管理,并且不要搞乱hibernate.current_session_context_class,因为这会破坏正确的集成。
  3. 你的for循环有点有问题,每处理x条记录后(最好与批处理大小设置相同),应该清除实体管理器并刷新。
- M. Deinum
显示剩余8条评论
4个回答

29

在尝试了所有可能的解决方案后,我终于找到了一种在5秒内插入10万行的解决方案!

我尝试的方法如下:

1)使用AtomicInteger生成自定义ID代替Hibernate/数据库的AUTOINCREMENT/GENERATED ID。

2)启用batch_inserts并设置batch_size=50。

3)每进行'batch_size'个persist()调用后刷新缓存。

4)多线程(我没有尝试这个方法)。

最终有效的方法是使用本地multi-insert查询,在一个SQL insert查询中插入1000行而不是在每个实体上使用persist()。对于插入100,000个实体,我创建了类似于以下格式的本地查询:"INSERT into MyTable VALUES (x,x,x),(x,x,x).......(x,x,x)" [在一个SQL insert查询中插入1000行]

现在插入100,000条记录只需要大约3秒钟!因此瓶颈在ORM本身上!对于批量插入,似乎唯一可行的方法是使用本地插入查询!


10
如果你设置了连接的rewriteBatchedStatements=true,你就不必自己重写应用程序,因为你自己完成的原生多插入查询改写,MySQL/MariaDB驱动程序可以自动执行。 - Per Huss
在我的persistence.xml中添加<property name="reWriteBatchedInserts" value="true">后,我没有看到它在我的日志中组合。此文章也不清楚从哪里获取PGSimpleDatasource:https://vladmihalcea.com/postgresql-multi-row-insert-rewritebatchedinserts-property/ - Zon
1
@PerHuss,使用您的本地查询方法,如果某些插入由于完整性约束违规而失败,会发生什么情况? - Simrandeep Singh
你将如何处理 SQL 注入问题? - karthik selvaraj
我不敢相信它的速度如此之快,我使用了rewriteBatchedStatements,它在8秒内插入了17,000条记录。 - undefined

7
  1. 您正在使用Spring来管理事务,但是通过使用 thread 作为当前会话上下文来打破它。在使用Spring管理事务时,请不要搞乱hibernate.current_session_context_class属性。请将其删除。

  2. 不要使用DriverManagerDataSource,请使用适当的连接池,例如HikariCP。

  3. 在您的 for 循环中,您应该定期使用flushclear清除EntityManager,最好与批处理大小相同。如果不这样做,单个持续时间会变得越来越长,因为当您这样做时,Hibernate会检查脏对象的一级缓存,对象越多,花费的时间就越多。对于10或100个对象,这是可以接受的,但是为每个持续操作检查数千个对象将产生负担。

@Service
@Transactional
public class ServiceImpl implements MyService{

    @Autowired
    private MyDao dao;

    @PersistenceContext
    private EntityManager em;


    void foo(){
        int count = 0;
        for(MyObject d : listOfObjects_100000){
            dao.persist(d);
            count++;
            if ( (count % 30) == 0) {
               em.flush();
               em.clear();
            }    
        }
    }

如需更深入的解释,请查看此博客此博客


我已经尝试了(1)和(3),但没有任何效果。每插入10万条数据仍需要大约30秒的时间。 - Kumar Manish
看起来这就是最好的了?我会尝试多线程作为最后的手段。 - Kumar Manish
多线程肯定会有帮助。我的一个同事就这样做了,将一个ETL过程分成了10个子进程。收益大约是7倍。 - Lluis Martinez
多线程可能有所帮助,但也可能使事情变得更加复杂。这取决于需要处理的处理类型、使用的事务类型等等。永远不要低估单个线程的能力。 - M. Deinum
我认为它的意思是“多线程可能会有帮助,但也可能会使情况变得更糟”。 - OpenSource

2
另一个要考虑的选项是无状态会话

用于对数据库执行批量操作的面向命令的API。

无状态会话不实现一级缓存,也不与任何二级缓存交互,也不实现事务写入后自动检查脏数据,操作也不会级联到关联实例。无状态会话忽略集合。通过无状态会话执行的操作绕过了Hibernate的事件模型和拦截器。由于没有一级缓存,无状态会话容易受到数据别名效应的影响。

对于某些类型的事务,无状态会话可能比有状态会话略快。

相关讨论: 使用无状态会话进行批处理


但是我需要使用Hibernate的JPA API。因此需要使用EntityManager而不是Session。这是否可以通过EntityManager实现?也许我可以尝试从EntityManager禁用第一级缓存? - Kumar Manish

-1

哎呀。

你可以做很多事情来提高速度。

1.) 使用@DynamicInsert和@DynamicUpdate来防止数据库插入非空列和更新更改的列。

2.) 尝试直接将列插入到数据库中(而不使用hibernate),以查看hibernate是否真的是瓶颈。

3.) 使用sessionfactory,并且仅在每个e.g. 100次插入后提交您的事务。 或者仅打开和关闭事务一次,并在每个100次插入后刷新数据。

4.) 使用ID生成策略“序列”,并让Hibernate通过参数allocationsize预分配ID。

5.) 使用缓存。

如果未正确使用,这些解决方案中的某些可能具有时间上的劣势。 但是你有很多机会。


实际上会导致更差的性能,因为Hibernate需要为每行创建单个查询,并需要检查字段是否更改...所有这些都需要时间,甚至可能破坏批处理语句的可能性,从而导致单个语句而不是批处理语句。 - M. Deinum
是的 M. Deinum,可以但不必须!如果您有具有 BLOB 的列,则这些注释会为您提供更多性能,因为它们在未更改时不会更新!正如我所写:“一些选项可能会有时间上的劣势”。 - M46

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