背景
上一篇文章提到再jdbc.url中添加rewriteBatchedStatements=true使得大批量插入数据速度得到巨大的提升。在实际生产环境添加了这个参数后发现涉及大批量插入数据的功能速度提升很明显,而有些速度提升效果远远低于预期,我决定一探究竟。
排查步骤
耗时很长的原因分析
整个功能需要耗时五十秒,功能是先将数据从E文本中解析组织为实体类对象,然后调用Hibernate的入库功能,然后经过一些后置处理。通过统计发现入库的解析的时间很短,入库花费的时间很长。毕竟是生产级别的数据,难道是数据量太大导致总体入库的时间太长?抱着疑问,统计了一下单次批量入库的数据量,发现一共入库的数据6800条左右。执行批量插入应该不会花费这么久的时间。
从使用wireshark抓包的情况来看,插入的时候确实是执行了批量插入的操作。但是从抓包的情况来看,发现了很多update操作。通过对update语句分析,发现了一个规律,凡是使用了Hibernate UserType的类型的实体类,在入库的时候均会先插入再更新。
为了验证猜想,写了一个简单的demo:
实体类Student.java:
@Entity
@Table(name = "hb_student")
public class Student {
@Id
@GeneratedValue(generator = "uuid")
@GenericGenerator(name = "uuid", strategy = "uuid")
@Column(name = "ID")
private String id;
@Column(name = "NAME")
private String name;
@Column(name = "BIRTH_DATE")
private Timestamp birthDate;
@Column(name = "JOIN_DATE")
private Date joinDate;
@Column(name = "EMAIL")
@Type(type="hibernate.vo.usertype.StringDataListType")
private List<String> email;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Timestamp getBirthDate() {
return birthDate;
}
public void setBirthDate(Timestamp birthDate) {
this.birthDate = birthDate;
}
public Date getJoinDate() {
return joinDate;
}
public void setJoinDate(Date joinDate) {
this.joinDate = joinDate;
}
public List<String> getEmail() {
return email;
}
public void setEmail(List<String> email) {
this.email = email;
}
}
Hibernate自定义数据类型StringDataListType.java:
public class StringDataListType implements Serializable, UserType {
private static final int[] SQL_TYPES = {Types.VARCHAR};
@Override
public int[] sqlTypes() {
return SQL_TYPES;
}
@Override
public Class returnedClass() {
return List.class;
}
@Override
public boolean equals(Object x, Object y) throws HibernateException {
if (x == null || y == null) {
return false;
}
List aList = (List) x;
List bList = (List) y;
int size = aList.size();
if (size != bList.size()) {
return false;
}
for (int i = 0; i < size; i++) {
Object aObject = aList.get(i);
Object bObject = bList.get(i);
if (aObject == null && bObject == null) {
continue;
}
if (aObject == null && bObject != null) {
return false;
}
if (aObject != null && bObject == null) {
return false;
}
if (!aObject.equals(bObject)) {
return false;
}
}
return true;
}
@Override
public int hashCode(Object x) throws HibernateException {
return Objects.hashCode(x);
}
@Override
public Object nullSafeGet(ResultSet rs, String[] names, SharedSessionContractImplementor session, Object owner) throws HibernateException, SQLException {
String value = rs.getString(names[0]);
if (value == null) {
return null;
}
return JSONObject.parseArray(value, String.class);
}
@Override
public void nullSafeSet(PreparedStatement st, Object value, int index, SharedSessionContractImplementor session) throws HibernateException, SQLException {
if (value == null) {
st.setNull(index, Types.VARCHAR);
} else {
String str = JSONObject.toJSONString(value);
st.setString(index, str);
}
}
@Override
public Object deepCopy(Object value) throws HibernateException {
return null;
}
@Override
public boolean isMutable() {
return false;
}
@Override
public Serializable disassemble(Object value) throws HibernateException {
return null;
}
@Override
public Object assemble(Serializable cached, Object owner) throws HibernateException {
return null;
}
@Override
public Object replace(Object original, Object target, Object owner) throws HibernateException {
return null;
}
main方法:
public static void main(String[] args) {
long time1 = System.currentTimeMillis();
Configuration configuration = new Configuration().configure();
SessionFactory sessionFactory = configuration.buildSessionFactory();
Session session = sessionFactory.getCurrentSession();
session.beginTransaction();
for (int i = 1; i <= 2; i++) {
Student student = new Student();
student.setName("hero" + i);
student.setBirthDate(new Timestamp(System.currentTimeMillis()));
student.setJoinDate(new Date());
student.setEmail(Arrays.asList("1@qq.com", "2@qq.com"));
session.save(student);
if (i % 2 == 0) {
session.flush();
session.clear();
}
}
session.getTransaction().commit();
session.close();
sessionFactory.close();
System.out.println("一共耗时:"+(System.currentTimeMillis()-time1)/1000);
}
在执行程序的时候,打开wireshark进行抓包。发现执行了1次insert和2次update.
demo的执行情况也确实验证了我之前的前的猜想。到这里,我们已经可以断定主要是在批量插入后,又对每个包含自定义数据类型的实体进行了更新操作。我们都知道更新数据比插入数据更花费时间,当时据量很大时,花费的时间就很可观了。
使用Usertype的实体在插入后为何还要执行一次更行操作?
我决定继续深挖,找出导致此问题的具体原因,看看有没有办法可以避免。
我们采用逆推法,一步一步的去分析原因:
1、之所以会执行update操作,是每天记录都绑定一个UpdateEntityAction
2、接下来我们追踪UpdateEntityAction的来源,当在flush的时候,会判断是否会需要进行更新。
3、什么条件下才会需要更新呢?
private boolean isUpdateNecessary(final FlushEntityEvent event, final boolean mightBeDirty) {
final Status status = event.getEntityEntry().getStatus();
if ( mightBeDirty || status == Status.DELETED ) {
// 会进行脏检查,比对缓存中的对象和实体对象
dirtyCheck( event );
if ( isUpdateNecessary( event ) ) {
return true;
}
else {
if ( SelfDirtinessTracker.class.isInstance( event.getEntity() ) ) {
( (SelfDirtinessTracker) event.getEntity() ).$$_hibernate_clearDirtyAttributes();
}
event.getSession()
.getFactory()
.getCustomEntityDirtinessStrategy()
.resetDirty( event.getEntity(), event.getEntityEntry().getPersister(), event.getSession() );
return false;
}
}
else {
return hasDirtyCollections( event, event.getEntityEntry().getPersister(), status );
}
}
接下来我们在看dirtyCheck的具体逻辑,dirtyCheck就是比对对象缓存中的数值和原实体对象的值。通过打断点发现,缓存中的值缺乏email值,而这个值的类型正式我们自定义的数据类型。通过分析代码逻辑,也确实是由于缓存中的email数值为空,导致脏检查不通过。
4、缓存中的对象是在调用save方法的时候插入进入的,我们需要进一步查看save的时候发生了什么?
在插入缓存前进行了一次深拷贝,发现进行深拷贝后,Email的数值没有拷贝成功。深拷贝的逻辑主要是调用了Hibernate类型的deepCopy()函数。
而deepCopy函数主要是调用userType的deepCopy函数来进行深拷贝。
@Override
public Object deepCopy(Object value, SessionFactoryImplementor factory) throws HibernateException {
return getUserType().deepCopy( value);
}
我们再去检查自定义数据类型,发现deepyCopy没有进行实现。
再回过头去看前面抓发得到的sql,确实再insert语句中,email属性为空。至此,原因终于找到了。经过修改后,生产环境的速度提升了很多。