概述
在实际开发项目中,不乏Spring和Hibernate框架结合使用来进行开发的。一方面Hibernate和Spring框架经过多年的版本迭代、众多生产环境项目的验证,已经十分可靠。配合使用配置简单,开发快速。近期接触到两个项目,使用框架类似,但是其中一个项目在开发、测试过程中频繁出问题,锁的问题不断。此文也是试图找出两者的异同,以及为什么会出现这么多锁。为了便于区分,两个项目我们分别叫A项目、B项目(锁问题频发项目)。
温故而知新
- Hibernate的session
Hibernate中的session和Http的session其实是两码事,虽然都是指“会话”,但意义并不一样。Hibernate的session是指一次CRUD操作所产生的数据库连接会话。 - 乐观锁与悲观锁
乐观锁:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。乐观锁则认为其他用户企图改变你正在更改的对象的概率是很小的,因此乐观锁直到你准备提交所作的更改时才将对象锁住,当你读取以及改变该对象时并不加锁。可见乐观锁加锁的时间要比悲观锁短,乐观锁可以用较大的锁粒度获得较好的并发访问性能。在乐观锁环境中,会增加并发用户读取对象的次数。
悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。悲观锁假定其他用户企图访问或者改变你正在访问、更改的对象的概率是很高的,因此在悲观锁的环境中,在你开始改变此对象之前就将该对象锁住,并且直到你提交了所作的更改之后才释放锁。悲观的缺陷是加锁的时间可能会很长,这样可能会长时间的限制其他用户的访问,也就是说悲观锁的并发访问性不好。 -
@Transactional
注解
该注解是Spring框架中提供的注解,项目使用中一般用在service层对应的类名上,进行事务控制。Transaction注解有个propagation属性,即事务的转播性,常用的是Propagation.REQUIRED
,这也是该属性的默认值,如下所示:
/**
* Support a current transaction, create a new one if none exists.
* Analogous to EJB transaction attribute of the same name.
* This is the default setting of a transaction annotation.
*/
REQUIRED(TransactionDefinition.PROPAGATION_REQUIRED)
知己知彼
- 配置
由于项目都是是用Hibernate和Spring来做事务控制、ORM,A、B项目相关XML配置中都是使用:
- 代码实现
A项目:
Dao层对数据库进行操作时,是通过SessionFactory.getCurrentSession()
拿到session,并调用saveOrUpdate()
方法来进行数据的新增或者更新[1]。
B项目:
该项目使用了第三方封装库,对实体Bean、数据库操作Dao都做了相应的底层封装,同时提供了视图操作、表操作相对应的简单方法以供调用,为了方便说明,我们看一些代码片段:
@Transactional(
rollbackFor = {Exception.class}
)
public abstract class BaseManager {
...
public void saveOrUpdate(E entity) {
this.getEntityDao().saveOrUpdate(entity);
}
...
}
每个Service都会去继承BaseManager
这个抽象类,自然也就可以调用父类中的saveOrUpdate()
来保存更新数据。
public void saveOrUpdate(E entity) {
if(entity instanceof BaseTableEntity) {
((BaseTableEntity)entity).updCommonProperties();
}
this.getHibernateTemplate().saveOrUpdate(entity);
this.getHibernateTemplate().flush();
}
而最终执行代码其实是使用的Hibernate提供的模版方法来进行数据保存更新,我们继续跟踪代码,往下看。
public class HibernateTemplate implements HibernateOperations, InitializingBean {
...
@Override
public void saveOrUpdate(final Object entity) throws DataAccessException {
executeWithNativeSession(new HibernateCallback
这里调用了executeWithNativeSession()
方法,参数是一个回调。该方法的内部会调用doExecute
获取session。
/**
* Execute the action specified by the given action object within a Session.
* @param action callback object that specifies the Hibernate action
* @param enforceNativeSession whether to enforce exposure of the native
* Hibernate Session to callback code
* @return a result object returned by the action, or {@code null}
* @throws org.springframework.dao.DataAccessException in case of Hibernate errors
*/
protected T doExecute(HibernateCallback action, boolean enforceNativeSession) throws DataAccessException {
Assert.notNull(action, "Callback object must not be null");
Session session = null;
boolean isNew = false;
try {
session = getSessionFactory().getCurrentSession();
}
catch (HibernateException ex) {
logger.debug("Could not retrieve pre-bound Hibernate session", ex);
}
if (session == null) {
session = getSessionFactory().openSession();
session.setFlushMode(FlushMode.MANUAL);
isNew = true;
}
try {
enableFilters(session);
Session sessionToExpose =
(enforceNativeSession || isExposeNativeSession() ? session : createSessionProxy(session));
return action.doInHibernate(sessionToExpose);
}
catch (HibernateException ex) {
throw SessionFactoryUtils.convertHibernateAccessException(ex);
}
catch (RuntimeException ex) {
// Callback code threw application exception...
throw ex;
}
finally {
if (isNew) {
SessionFactoryUtils.closeSession(session);
}
else {
disableFilters(session);
}
}
}
其中当session等于null
的时候,会通过openSession()
方法获取一个新的session,并且设置他的FlushMode
为FlushMode.MANUAL
,这里很关键(敲黑板,划重点)[2]。
- 异常日志分析
org.springframework.orm.hibernate4.HibernateOptimisticLockingFailureException:
Object of class [com.*.model.EquipmentState] with identifier [2]: optimistic locking failed;
nested exception is org.hibernate.StaleObjectStateException:
Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [com.*.model.EquipmentState#2]
at org.springframework.orm.hibernate4.SessionFactoryUtils.convertHibernateAccessException(SessionFactoryUtils.java:202)
at org.springframework.orm.hibernate4.HibernateTemplate.doExecute(HibernateTemplate.java:344)
at org.springframework.orm.hibernate4.HibernateTemplate.executeWithNativeSession(HibernateTemplate.java:309)
at org.springframework.orm.hibernate4.HibernateTemplate.flush(HibernateTemplate.java:838)
这里截取了典型错误日志的部分内容来进行分析。很明显,这里报了一个乐观锁的错误。这时,我们回头看FlushMode.MANUAL
的设置,是不是能发现点什么?这里设置为手动的方式,当A线程操作更新的时候,数据已经设置为readyonly
。这时候B线程过来操作更新数据,在目前没有印证代码使用的是乐观锁,仅仅通过异常日志反推,在乐观锁的情况下,B线程发现锁,稍微等待就立即返回,抛出乐观锁的HibernateOptimisticLockingFailureException
异常。
- 印证
假如使用了乐观锁,肯定有乐观锁的对应配置,因为Hibernate默认不会使用乐观锁。根据乐观锁的特殊性,项目中如果要使用乐观锁,对数据操作的时候都要有Retry相关的代码逻辑来确保更新或者保存成功。另外,在数据库设计时也应该有对应的标识来去用来判断“版本”,一般采用一个单独的version列,或者使用时间戳。因为B项目是使用的Hibernate注解的方式来加载bean,而项目中bean的定义十分复杂(上文中已经讲过,框架层对bean有做封装),那么别急,既然是注解,我们就找@Version
或者@Timestamp
,一步步缩小范围——找到了!
@Version
@Column(
name = "EXCLUSIVE_KEY",
unique = false,
nullable = true,
insertable = true,
updatable = true
)
public Long getExclusiveKey() {
return this.exclusiveKey;
}
在框架中的BaseTableEntity
底层类中定义了表的必须列EXCLUSIVE_KEY
,而他的@Version
注解也解释了该列的作用,相互印证,所有问题都说得通了。由于B项目在底层框架就已经选型“乐观锁”,但是实际业务代码中却没有相应的Retry措施,也没有对乐观锁相关的异常进行进一步的处理,那么在一些关键业务上,只能卡壳,无法正常跑通流程。
- [1] 一般我们是通过
SessionFactory.openSession()
方法拿到原生session(重新建立一个会话),并通过session去做对应的CRUD操作。通过openSession()
方法拿到的session需要在提交后调用Session.close()
方法关闭session。如果不进行关闭,最终只会撑爆数据库连接池。需要特别注意的是,openSession()
方法并不是线程安全的,实际项目中通常使用线程安全的SessionFactory.getCurrentSession()
方法去获取当前线程所绑定的session,而且session会在事务提交后自动关闭,无需开发人员手动调用Session.close()
方法,省时省心。
- [2] Hibernate session FlushMode有五种属性:
- NEVEL:已经废弃了,被MANUAL取代了
- MANUAL:如果FlushMode是MANUAL或NEVEL,在操作过程中hibernate会将事务设置为readonly,所以在增加、删除或修改操作过程中会出现如下错误
org.springframework.dao.InvalidDataAccessApiUsageException: Write operations are not allowed in read-only mode (FlushMode.NEVER) - turn your Session into FlushMode.AUTO or remove 'readOnly' marker from transaction definition;
解决办法:配置事务,spring会读取事务中的各种配置来覆盖hibernate的session中的FlushMode;- AUTO设置成auto之后,当程序进行查询、提交事务或者调用session.flush()的时候,都会使缓存和数据库进行同步,也就是刷新数据库;
- COMMIT提交事务或者session.flush()时,刷新数据库,查询不刷新;
- ALWAYS:每次进行查询、提交事务、session.flush()的时候都会刷数据库ALWAYS和AUTO的区别:当hibernate缓存中的对象被改动之后,会被标记为脏数据(即与数据库不同步了)。当 session设置为FlushMode.AUTO时,hibernate在进行查询的时候会判断缓存中的数据是否为脏数据,是则刷数据库,不是则不刷,而always是直接刷新,不进行任何判断。很显然auto比always要高效得多。