在编写企业应用程序时,通常将工作分配在服务于典型OLTP(在线事务处理)传输的前端系统,和一个或多个批处理用于ETL(Extract,Transform,Load)操作。
批处理器,顾名思义,将要处理的数据分成几个块,因此具有以下优点:
每个块可以通过单独的工作线程进行处理,因此增加了吞吐量并减少了整个处理时间。
每个块都可以使用自己的数据库事务,所以如果有一个失败,就不用丢掉我们所做的所有工作,只是针对当前事务的变化。
JPA 批处理当使用JPA时,假设要插入50个Post实体,那么应该这样做:
int entityCount = 50; int batchSize = 25; EntityManager entityManager = null; EntityTransaction transaction = null; try { entityManager = entityManagerFactory() .createEntityManager(); transaction = entityManager.getTransaction(); transaction.begin(); for ( int i = 0; i < entityCount; ++i ) { if ( i > 0 && i % batchSize == 0 ) { entityManager.flush(); entityManager.clear(); transaction.commit(); transaction.begin(); } Post post = new Post( String.format( "Post %d", i + 1 ) ); entityManager.persist( post ); } transaction.commit(); } catch (RuntimeException e) { if ( transaction != null && transaction.isActive()) { transaction.rollback(); } throw e; } finally { if (entityManager != null) { entityManager.close(); } }每个开始操作都会开启事务,因为每个实体状态转换必须在数据库事务的范围内执行。
for循环一次会持久化一个Post实体。 但是,由于实体状态转换仅在执行flush方法更新数据库时执行,因此我们可以将多个SQL 插入语句分组到到要给单一PreparedStatement执行中,该执行需要多个参数。
每次迭代计数器(变量i)已达到batchSize临界值的倍数,我们可以刷新EntityManager并提交数据库事务。 通过在每次批处理执行后提交数据库事务,我们获得以下优点:
避免了长期运行的事务,这对MVCC关系数据库系统是不利的。
我们确保如果执行失败,不会丢失以前成功执行的批处理作业完成的工作。
EntityManager在每次批量执行后被清除,这样就不会继续累积可能导致管理实体的几个问题:
如果要持久化的实体数量庞大,那么存在内存不足的风险。
在持久化上下文中累积的实体越多,flush越慢。 所以,最好的做法是确保持久性上下文尽可能的短小。
如果抛出异常,我们必须确保回滚当前正在运行的数据库事务。 否则可能会导致许多问题,因为数据库可能仍然认为事务处于打开状态,锁可能会被持有,直到事务超时或由DBA结束。
最后,我们需要关闭EntityManager,以便可以清除上下文并释放Session级的资源。
虽然这是使用JPA进行批处理的正确方法,但还没有完成。 如前所述,也可以从JDBC批量更新中受益。 为此,我们需要提供以下Hibernate配置属性:
<property name="hibernate.jdbc.batch_size" value="25" /> <property name="hibernate.order_inserts" value="true" /> <property name="hibernate.order_updates" value="true" />这些属性允许我们将多个SQL语句批处理为单个PreparedStatement执行,这需要单个数据库往返。 选择值25以匹配EntityManager批处理作业的临界值。