一次解“锁”之旅

概述

在实际开发项目中,不乏Spring和Hibernate框架结合使用来进行开发的。一方面Hibernate和Spring框架经过多年的版本迭代、众多生产环境项目的验证,已经十分可靠。配合使用配置简单,开发快速。近期接触到两个项目,使用框架类似,但是其中一个项目在开发、测试过程中频繁出问题,锁的问题不断。此文也是试图找出两者的异同,以及为什么会出现这么多锁。为了便于区分,两个项目我们分别叫A项目、B项目(锁问题频发项目)。

温故而知新

  1. Hibernate的session
    Hibernate中的session和Http的session其实是两码事,虽然都是指“会话”,但意义并不一样。Hibernate的session是指一次CRUD操作所产生的数据库连接会话。
  2. 乐观锁与悲观锁
    乐观锁:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。乐观锁则认为其他用户企图改变你正在更改的对象的概率是很小的,因此乐观锁直到你准备提交所作的更改时才将对象锁住,当你读取以及改变该对象时并不加锁。可见乐观锁加锁的时间要比悲观锁短,乐观锁可以用较大的锁粒度获得较好的并发访问性能。在乐观锁环境中,会增加并发用户读取对象的次数。
    悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。悲观锁假定其他用户企图访问或者改变你正在访问、更改的对象的概率是很高的,因此在悲观锁的环境中,在你开始改变此对象之前就将该对象锁住,并且直到你提交了所作的更改之后才释放锁。悲观的缺陷是加锁的时间可能会很长,这样可能会长时间的限制其他用户的访问,也就是说悲观锁的并发访问性不好。
  3. @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)

知己知彼

  1. 配置
    由于项目都是是用Hibernate和Spring来做事务控制、ORM,A、B项目相关XML配置中都是使用:

  1. 代码实现
    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() {
                @Override
                public Object doInHibernate(Session session) throws HibernateException {
                    checkWriteOperationAllowed(session);
                    session.saveOrUpdate(entity);
                    return null;
                }
            });
        }

        ...
}
 
 

这里调用了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,并且设置他的FlushModeFlushMode.MANUAL,这里很关键(敲黑板,划重点)[2]

  1. 异常日志分析
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异常。

  1. 印证
    假如使用了乐观锁,肯定有乐观锁的对应配置,因为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有五种属性:
  1. NEVEL:已经废弃了,被MANUAL取代了
  2. 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;
  3. AUTO设置成auto之后,当程序进行查询、提交事务或者调用session.flush()的时候,都会使缓存和数据库进行同步,也就是刷新数据库;
  4. COMMIT提交事务或者session.flush()时,刷新数据库,查询不刷新;
  5. ALWAYS:每次进行查询、提交事务、session.flush()的时候都会刷数据库ALWAYS和AUTO的区别:当hibernate缓存中的对象被改动之后,会被标记为脏数据(即与数据库不同步了)。当 session设置为FlushMode.AUTO时,hibernate在进行查询的时候会判断缓存中的数据是否为脏数据,是则刷数据库,不是则不刷,而always是直接刷新,不进行任何判断。很显然auto比always要高效得多。

你可能感兴趣的:(一次解“锁”之旅)