Hibernate 安插海量数据时的性能 与 Jdbc 的比较

Hibernate 插入海量数据时的性能 与 Jdbc 的比较
系统环境:

Hibernate 安插海量数据时的性能 与 Jdbc 的比较


MySQL 数据库环境:
mysql> select version();
--------------
select version()
--------------

+----------------------+
| version()            |
+----------------------+
| 5.1.41-community-log |
+----------------------+
1 row in set (0.00 sec)


eclipse 运行时参数:
--launcher.XXMaxPermSize
256M
-showsplash
org.eclipse.platform
--launcher.XXMaxPermSize
256m
-vm
C:/Program Files/Java/jdk1.6.0_18/bin/javaw
-vmargs
-Dosgi.requiredJavaVersion=1.5
-Xms40m
-Xmx512m
 


使用 Spring 2.5、Hibernate3.3

Entity:

package com.model;

import java.io.Serializable;
import java.sql.Timestamp;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

/**
 * @author <a href="liu.anxin13@gmail.com">Tony</a>
 */
@Entity
@Table(name = "T_USERINFO")
@org.hibernate.annotations.Entity(selectBeforeUpdate = true, dynamicInsert = true, dynamicUpdate = true)
public class UserInfo implements Serializable {

	private static final long serialVersionUID = -4855456169220894250L;

	@Id
	@Column(name = "ID", length = 32)
	private String id = java.util.UUID.randomUUID().toString().replaceAll("-", "");

	@Column(name = "CREATE_TIME", updatable = false)
	private Timestamp createTime = new Timestamp(System.currentTimeMillis());

	@Column(name = "UPDATE_TIME", insertable = false)
	private Timestamp updateTime = new Timestamp(System.currentTimeMillis());
	// setter/getter...
}


applicationContext.xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns="http://www.springframework.org/schema/beans"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:tx="http://www.springframework.org/schema/tx"
	xmlns:aop="http://www.springframework.org/schema/aop"
	xsi:schemaLocation="http://www.springframework.org/schema/beans 
		http://www.springframework.org/schema/beans/spring-beans-2.5.xsd 
		http://www.springframework.org/schema/context 
		http://www.springframework.org/schema/context/spring-context-2.5.xsd 
		http://www.springframework.org/schema/tx 
		http://www.springframework.org/schema/tx/spring-tx-2.5.xsd
		http://www.springframework.org/schema/aop
		http://www.springframework.org/schema/aop/spring-aop-2.5.xsd">

	<context:component-scan base-package="com.dao,com.service" />

	<context:property-placeholder location="classpath:jdbc.properties" />

	<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"
		destroy-method="close">
		<property name="driverClass" value="${jdbc.driver}" />
		<property name="jdbcUrl" value="${jdbc.url}" />
		<property name="user" value="${jdbc.username}" />
		<property name="password" value="${jdbc.password}" />
		
		<property name="maxPoolSize" value="50" />
		<property name="minPoolSize" value="2" />
		<property name="initialPoolSize" value="5" />
		<property name="acquireIncrement" value="5" />
		<property name="maxIdleTime" value="1800" />
		<property name="idleConnectionTestPeriod" value="1800" />
		<property name="maxStatements" value="1000"/>
		<property name="breakAfterAcquireFailure" value="true" />
		<property name="testConnectionOnCheckin" value="true" />
		<property name="testConnectionOnCheckout" value="false" />
	</bean>
	
	<bean id="sessionFactory" class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean">
		<property name="dataSource" ref="dataSource"/>
		<property name="configLocation" value="classpath:hibernate.cfg.xml" />
	</bean>
	
	<bean id="hibernateTemplate" class="org.springframework.orm.hibernate3.HibernateTemplate">
		<property name="sessionFactory" ref="sessionFactory" />
	</bean>
	
	<bean id="transactionManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager">
		<property name="sessionFactory" ref="sessionFactory" />
	</bean>
	
	<tx:advice id="txAdvice" transaction-manager="transactionManager">
		<tx:attributes>
			<tx:method name="*" isolation="READ_COMMITTED" rollback-for="Throwable" />
		</tx:attributes>
	</tx:advice>
	
	<aop:config>
		<aop:pointcut id="services" expression="execution(* com.service.*.*.*.*(..))" />
		<aop:advisor advice-ref="txAdvice" pointcut-ref="services" />
	</aop:config>

</beans>


hibernate.cfg.xml:
<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE hibernate-configuration PUBLIC
          "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
          "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">

<hibernate-configuration>

	<session-factory>

		<property name="hibernate.dialect">
			org.hibernate.dialect.MySQLDialect
		</property>
		
		<!--
			默认 15, 这是我个人不能完全理解的参数, 不明白这个参数的实际意义
		-->
		<!-- <property name="hibernate.jdbc.batch_size">50</property> -->
		
		<!-- 排序插入和更新, 避免出现 锁si -->
		<property name="hibernate.order_inserts">true</property>
		<property name="hibernate.order_updates">true</property>

		<property name="hibernate.hbm2ddl.auto">update</property>

		<property name="hibernate.show_sql">false</property>
		<property name="hibernate.format_sql">false</property>
		
		<property name="hibernate.current_session_context_class">
			org.hibernate.context.JTASessionContext
		</property>

		<mapping class="com.model.UserInfo" />

	</session-factory>

</hibernate-configuration>


log4j.properties:
log4j.rootLogger=INFO, CONS
log4j.appender.CONS=org.apache.log4j.ConsoleAppender
log4j.appender.CONS.layout=org.apache.log4j.PatternLayout
log4j.appender.CONS.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss sss} [%p] %r %l [%t] %x - %m%n

# log4j.logger.org.springframework=WARN
# log4j.logger.org.hibernate=WARN
# log4j.logger.com.mchange=WARN


HibernateDAO.java:
package com.dao;

import java.io.Serializable;

import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.orm.hibernate3.HibernateTemplate;

@Repository
public class HibernateDAO {

	private static final Logger log = Logger.getLogger(HibernateDAO.class);

	@Autowired
	private HibernateTemplate template;

	public Serializable save(Object entity) {
		try {
			return template.save(entity);
		} catch (Exception e) {
			log.info("save exception : " + e.getMessage());
			// 异常的目的只是为了记录日志
			throw new RuntimeException(e);
		}
	}

}


CommonServiceImpl.java:
package com.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.dao.HibernateDAO;
import com.model.UserInfo;

@Service("commonService")
public class CommonService {

	private static final Logger log = Logger.getLogger(CommonService.class);

	@Autowired
	private HibernateDAO dao;

	public void testOcean(long num) {
		for (int i = 0; i < num; i++) {
			// 在循环中 new 大量对象. 此为 outOfMemory 的根源
			// 将 user 申明在 循环外面, 循环体内部才将其指向具体的对象, 这样可以提高一点效率
			UserInfo user = new UserInfo();
			dao.save(user);
			
			user = null;
			// 实际意义不大, 就算手动运行, gc 也是异步的, 没有办法控制
			if (i % 100000 == 0)
				System.gc();
			/*
			if (i % 30 == 0) {
				dao.flush();
				dao.clear();
			}
			*/
		}
	}
	
}


Test.java:
package com;

import org.apache.log4j.Logger;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import com.service.CommonService;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "/applicationContext.xml" })
public class TestHibernate {

	@Autowired
	@Qualifier("commonService")
	private CommonService service;
	
	private static final Logger log = Logger.getLogger(TestHibernate.class);
	
	@Test
	public void testHibernate() {
		// 海量数据的条数. 分别以 1W, 10W, 50W, 100W 为数值
		// 虽然都算不上海量, 但做为测试也应该算够了吧
		long num = 10000;
		
		log.info("开始");
		long begin = System.currentTimeMillis();
		service.testOcean(num);

		long end = System.currentTimeMillis();
		log.info("耗费时间: " + num + " 条. " + (end - begin) / 1000.00000000 + " 秒");
		System.out.println("赶紧查看内存");
		try {
			Thread.sleep(10000);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
}



结果:

SQL 生成语句如下:

Hibernate: 
    insert 
    into
        T_USERINFO
        (CREATE_TIME,  ID) 
    values
        (?, ?)

 
不使用批量

1W: 耗费时间: 3.65 秒   内存占用见下图

Hibernate 安插海量数据时的性能 与 Jdbc 的比较

10W: 耗费时间: 33.844 秒
Hibernate 安插海量数据时的性能 与 Jdbc 的比较

50W : outOfMemory


30条为一个周期, 也就是将 if (i % 30 == 0) 这段注释解开

1W: 耗费时间: 3.797 秒. 查看内存, 这个时间就明显的感觉到运行时的压力已经有一些转换到数据库上面来了(运行时看得更清楚一些)

Hibernate 安插海量数据时的性能 与 Jdbc 的比较

MySQL 的进程没能抓到图, 而且其是在运行时耗的资源. 也并不是很大, 比上图要小

10W: 耗费时间: 34.922 秒

Hibernate 安插海量数据时的性能 与 Jdbc 的比较

50W: 耗费时间: 182.531 秒
Hibernate 安插海量数据时的性能 与 Jdbc 的比较

100W outOfMemory


50条为一个周期, 即将上面的 30 改为 50

1W: 耗费时间: 3.625 秒

Hibernate 安插海量数据时的性能 与 Jdbc 的比较

10W: 耗费时间: 32.031 秒
Hibernate 安插海量数据时的性能 与 Jdbc 的比较

50W: 耗费时间: 161.391 秒
Hibernate 安插海量数据时的性能 与 Jdbc 的比较


100W 同样 outOfMemory .


一路看下来, 我并不觉得 Hibernate 有多慢. 4秒以内处理 1W 条数据 . 至于内存溢出, 还是因为在 循环里面 new 了太多的对象, 尽管处理完我就将其引用指向 null 并显式调用 gc , 但 JVM 内部运行 gc 是异步的...

当然 性能上使用 50批量 的策略会好很多, 可以将压力让 服务器 和 数据库 同时去承担 .


保存下来再去测试 jdbc 批量看看...

使用 jdbc 测试:

在 applicationContext.xml 中添加:

<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
	<property name="dataSource" ref="dataSource" />
</bean>


JdbcDAO.java:
package com.dao;

import java.sql.Connection;

import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

@Repository
public class JdbcDAO {
	
	private static final Logger log = Logger.getLogger(JdbcDAO.class);
	
	@Autowired
	private JdbcTemplate template;
	
	public Connection getConn() {
		try {
			return template.getDataSource().getConnection();
		} catch (Exception e) {
			log.info("获取连接时异常: " + e.getMessage());
			throw new RuntimeException(e);
		}
	}

}


JdbcService.java:
package com.service;

import java.sql.Connection;
import java.sql.PreparedStatement;

import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.dao.JdbcDAO;
import com.model.UserInfo;

@Service("jdbcService")
public class JdbcService {
	
	private static final Logger log = Logger.getLogger(JdbcService.class);
	
	@Autowired
	private JdbcDAO dao;

	public void testOcean(long num) {
		Connection conn = dao.getConn();
		try {
			conn.setAutoCommit(false);
			String sql = "insert into T_USERINFO(CREATE_TIME, ID) values(?, ?)";
			PreparedStatement pstm = conn.prepareStatement(sql);
			for (int i = 0; i < num; i++) {
				// 要保证公平, 也在循环中 new 对象
				UserInfo user = new UserInfo();
				pstm.setTimestamp(1, user.getCreateTime());
				pstm.setString(2, user.getId());
				pstm.execute();
				
				user = null;
				if (i % 10000 == 0)
					System.gc();
				// 批处理
				/*
				if (i % 30 == 0) {
					pstm.executeBatch();
					conn.commit();
					pstm.clearBatch();
				}
				*/

			}
			// 将循环里面的批处理解开后, 就要将此处的commit注释, 并将下面注释的语句解开
			conn.commit();
			// pstm.executeBatch();
		} catch (Exception e) {
			log.info("异常: " + e.getMessage());
		} finally {
			try {
				conn.close();
			} catch (Exception e) {
				conn = null;
			}
		}
	}

}


TestJdbc.java: (这里很郁闷, 我用的 Junit 4.6 版本, 在上面的测试中正常. 在这里异常, 换成 4.4 就好了)
package com;

import org.apache.log4j.Logger;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import com.service.JdbcService;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "/applicationContext.xml" })
public class TestJdbc {
	
	private static final Logger log = Logger.getLogger(TestJdbc.class);
	
	@Autowired
	@Qualifier("jdbcService")
	private JdbcService service;

	@Test
	public void testJdbc() {
		long num = 500000;
		
		log.info("开始");
		long begin = System.currentTimeMillis();
		service.testOcean(num);
		long end = System.currentTimeMillis();
		log.info("耗费时间: " + num + " 条. " + (end - begin) / 1000.00000000 + " 秒");
		
		System.out.println("赶紧查看内存");
		try {
			Thread.sleep(10000);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

}

 

结果如下:

不使用批量:

1W: 耗费时间: 3.359 秒(这个效率确实比 Hibernate 要高)

Hibernate 安插海量数据时的性能 与 Jdbc 的比较

10W: 耗费时间: 212.812 秒 (我不确定是不是我 jdbc的代码写得不够好, 反正这个效率让我很是郁闷)
Hibernate 安插海量数据时的性能 与 Jdbc 的比较

50W 的时候我已经没有耐心再等下去了.  11 分钟过去了还没看到结果


使用30条一次批量

说实话, 这个时候的性能已经让我到了一种发狂的地步.
接近 3 秒 运行 100 条 . 1W 条数据花了 288.266 秒的时间
是的, 你没有看错, 就是 3 秒运行 100 条, 到后面的 50 我也就没有再继续测下去.

也可能是我 jdbc 代码写得不对吧. 毕竟 hibernate 底层也是用的 jdbc, 其再快也是不可能快过 Native SQL, 我这个贴的目的也只是想了解下差距的多少, 只是这个效率...

如果哪位有过 jdbc 方面的操作, 给个提醒...
谢谢.


1 楼 peterwei 2010-08-22  
你的用法有问题吧,这个要靠你多试试多种情况。我的机子比你差多了。还没你说的情况。我的测试如下:
================================
清空表
通过PrepareStatement插入数据:
插入数据量:500000
<运行时间: 96.266 秒>
运行时间:96266 毫秒
96.266
================================
清空表
用批处理插入数据:
批量更新成功 500000 条记录!
<运行时间: 96.515 秒>
运行时间:96515 毫秒
96.515
================================
你可以看看我的blog,我也写了个测试jdbc性能的。
2 楼 peterwei 2010-08-22  
我的机器配置是1.8G的老机子,差不多800M内存。你想想都比你的快那么多,肯定是你哪写得不对了。
1.if (i % 10000 == 0)  
                    System.gc(); 这个没什么用吧

2.没看见有ps.addBatch();我的有的,而用ps不用批处理时用的是executeUpdate();

3.就算我在里面new东西,50w也只是多了10秒不到。

你可以仔细看一下我的代码。
3 楼 yhailj 2010-08-22  
peterwei 写道
我的机器配置是1.8G的老机子,差不多800M内存。你想想都比你的快那么多,肯定是你哪写得不对了。
1.if (i % 10000 == 0)  
                    System.gc(); 这个没什么用吧

2.没看见有ps.addBatch();我的有的,而用ps不用批处理时用的是executeUpdate();

3.就算我在里面new东西,50w也只是多了10秒不到。

你可以仔细看一下我的代码。



1. 我在测试 Hibernate 的时候写过
// 实际意义不大, 就算手动运行, gc 也是异步的, 没有办法控制
if (i % 10000 == 0)   
    System.gc(); 

以上代码也只是想让 gc 整理 循环体内部因为申明太多指向跟 new 太多对象造成的内存溢出

2. 的确, 直接执行 pstm.execute(); 是直接执行, 不是批量了! 如果要批量的话, 应该像下面这样:
for (int i = 0; i < num; i++) {
	// 要保证公平, 也在循环中 new 对象
	UserInfo user = new UserInfo();
	pstm.setTimestamp(1, user.getCreateTime());
	pstm.setString(2, user.getId());

	pstm.addBatch();

	user = null;
	if (i % 10000 == 0)
		System.gc();

	// 批处理
	if (i % 30 == 0) {
		pstm.executeBatch();
		conn.commit();
		pstm.clearBatch();
	}

}
pstm.executeBatch();
conn.commit();


哪天我再测一下, 这还是去年在公司测的. 一直没动过
4 楼 bushkarl 2010-08-23  
Hibernate 安插海量数据时的性能 与 Jdbc 的比较