Spring事务使用场景

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/peerless_hero/article/details/77509203

常规调用

@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
methodA{
    ……
    try{
        methodB();
    } catch (Exception e) {
        ……
    }
    localA();
    ……
}

@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
methodB{
   ……
}

methodA调用methodB时,没有用try-catch语句包住,事务提交/回滚的结果如下:

场景 同一个类 非同一个类
localA异常 回滚 回滚
methodB捕获异常/无异常 提交 提交
methodB抛异常 回滚 回滚

methodA调用methodB时,用try-catch语句包住,事务提交/回滚的结果如下:

场景 同一个类 非同一个类
localA异常 回滚 回滚
methodB捕获异常/无异常 提交 提交
methodB抛异常 提交 回滚

这里要重点强调下两个method是否写在同一个类中的区别。
笔者这里是通过在配置文件中添加<tx:annotation-driven/> 来支持事务注解的,该标签会查找和它在相同的应用上下文件中定义的bean上面的@Transactional注解。打上@Transactional注解的类会生成一个代理类,调用时会被事务切面锁拦截。而如果写在同一个类中,methodA是通过this指针调用methodB,这样不会被事务切面所拦截的。

一、当事务方法被切面拦截后,是如何开启Spring事务的呢?
这里写图片描述

createTransactionIfNecessary方法会根据事务属性有需要地创建一个事务。实际的事务创建是在doBegin方法中,

    /**
     * This implementation sets the isolation level but ignores the timeout.
     */
    @Override
    protected void doBegin(Object transaction, TransactionDefinition definition) {
        DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
        Connection con = null;

        try {
            if (txObject.getConnectionHolder() == null ||
                    txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
                // 创建Connection 
                Connection newCon = this.dataSource.getConnection();
                if (logger.isDebugEnabled()) {
                    logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction");
                }
                // 根据Connection创建ConnectionHolder
                txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
            }

            txObject.getConnectionHolder().setSynchronizedWithTransaction(true);
            con = txObject.getConnectionHolder().getConnection();

            Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition);
            txObject.setPreviousIsolationLevel(previousIsolationLevel);

            // Switch to manual commit if necessary. This is very expensive in some JDBC drivers,
            // so we don't want to do it unnecessarily (for example if we've explicitly
            // configured the connection pool to set it already).
            if (con.getAutoCommit()) {
                txObject.setMustRestoreAutoCommit(true);
                if (logger.isDebugEnabled()) {
                    logger.debug("Switching JDBC Connection [" + con + "] to manual commit");
                }
                //将自动提交改为false
                con.setAutoCommit(false);
            }
            txObject.getConnectionHolder().setTransactionActive(true);

            int timeout = determineTimeout(definition);
            if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) {
                txObject.getConnectionHolder().setTimeoutInSeconds(timeout);
            }

            // Bind the session holder to the thread.
            if (txObject.isNewConnectionHolder()) {
                // 重头戏在这里,将ConnectionHolder绑定到当前线程
                TransactionSynchronizationManager.bindResource(getDataSource(), txObject.getConnectionHolder());
            }
        }

        catch (Throwable ex) {
            if (txObject.isNewConnectionHolder()) {
                DataSourceUtils.releaseConnection(con, this.dataSource);
                txObject.setConnectionHolder(null, false);
            }
            throw new CannotCreateTransactionException("Could not open JDBC Connection for transaction", ex);
        }
    }

创建Connection后,通过bindResource方法,将ConnectionHolder相关信息存入ThreadLocal对象中,这样ConnectionHolder和线程就绑定起来了。

    public static void bindResource(Object key, Object value) throws IllegalStateException {
        Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
        Assert.notNull(value, "Value must not be null");
        Map<Object, Object> map = resources.get();
        // set ThreadLocal Map if none found
        if (map == null) {
            map = new HashMap<Object, Object>();
            resources.set(map);
        }
        Object oldValue = map.put(actualKey, value);
        // Transparently suppress a ResourceHolder that was marked as void...
        if (oldValue instanceof ResourceHolder && ((ResourceHolder) oldValue).isVoid()) {
            oldValue = null;
        }
        if (oldValue != null) {
            throw new IllegalStateException("Already value [" + oldValue + "] for key [" +
                    actualKey + "] bound to thread [" + Thread.currentThread().getName() + "]");
        }
        if (logger.isTraceEnabled()) {
            logger.trace("Bound value [" + value + "] for key [" + actualKey + "] to thread [" +
                    Thread.currentThread().getName() + "]");
        }
    }

    private static final ThreadLocal<Map<Object, Object>> resources =
            new NamedThreadLocal<Map<Object, Object>>("Transactional resources");

我们查看下resources.get()中的数据,

{
    CreateTime:"2017-08-21 11:36:04",
    ActiveCount:1,
    PoolingCount:2,
    CreateCount:3,
    DestroyCount:0,
    CloseCount:0,
    ConnectCount:1,
    Connections:[
        {ID:103750239, ConnectTime:"2017-08-21 11:36:05", UseCount:0, LastActiveTime:"2017-08-21 11:36:05"},
        {ID:1397252899, ConnectTime:"2017-08-21 11:36:05", UseCount:0, LastActiveTime:"2017-08-21 11:36:05"}
    ]
}

二、如果没有Spring事务托管,仅通过mybatis框架是如何管理事务的呢?
mybatis框架本身也可以做事务管理,和Spring管理事务类似,mybatis将sessionHolder保存在ThreadLocal对象中,这样sessionHolder和线程也绑定了起来。

  public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {

    notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
    notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED);
    // 从ThreadLocal对象中获取SqlSessionHolder 
    SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);

    SqlSession session = sessionHolder(executorType, holder);
    if (session != null) {
      return session;
    }

    if (LOGGER.isDebugEnabled()) {
      LOGGER.debug("Creating a new SqlSession");
    }
    // 如果不存在SqlSessionHolder,则执行openSession创建session
    session = sessionFactory.openSession(executorType);
    // 将session存入ThreadLocal对象中
    registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);

    return session;
  }

三、spring事务管理+mybatis事务管理,ThreadLocal对象中的数据如何?

{{
    CreateTime:"2017-08-21 10:54:03",
    ActiveCount:1,
    PoolingCount:2,
    CreateCount:3,
    DestroyCount:0,
    CloseCount:0,
    ConnectCount:1,
    Connections:[
        {ID:748532070, ConnectTime:"2017-08-21 10:54:03", UseCount:0, LastActiveTime:"2017-08-21 10:54:03"},
        {ID:250688639, ConnectTime:"2017-08-21 10:54:03", UseCount:0, LastActiveTime:"2017-08-21 10:54:03"}
    ]
}=org.springframework.jdbc.datasource.ConnectionHolder@44df9749, org.apache.ibatis.session.defaults.DefaultSqlSessionFactory@4e44f87e=org.mybatis.spring.SqlSessionHolder@a8dab8e}

我们看到,resources.get()中的数据不仅包含了ConnectionHolder,又包含了SqlSessionHolder。而SqlSession又封装了Connection信息,我们认为ConnectionHolder和SqlSessionHolder都指向了同一个数据库连接。

四、我们来分析一下methodA在同一个类中调用methodB、methodB抛出异常的场景

methodA会开启Spring事务,methodB不走Spring事务,但methodB会从ThreadLocal对象中获取SqlSession信息,和methodA拥有同一个数据库连接。

(1) 如果methodA用try/catch语句块包住了methodB,
既然methodA会捕获异常,那么代码执行完毕会执行commitTransactionAfterReturning方法,最终会提交数据。

(2) 如果methodA没有用try/catch语句块包住了methodB,
那么代码执行完毕会执行completeTransactionAfterThrowing方法,最终会回滚数据。

五、我们再分析下methodA调用另一个类中的methodB方法、methodB捕获异常/无异常的场景

根据Spring事务传播性,methodA和methodB会加入同一个事务,由于执行过程无异常,methodA和methodB都会执行commitTransactionAfterReturning方法,但只会提交一次。

    private void processCommit(DefaultTransactionStatus status) throws TransactionException {
        try {
            boolean beforeCompletionInvoked = false;
            try {
                prepareForCommit(status);
                triggerBeforeCommit(status);
                triggerBeforeCompletion(status);
                beforeCompletionInvoked = true;
                boolean globalRollbackOnly = false;
                if (status.isNewTransaction() || isFailEarlyOnGlobalRollbackOnly()) {
                    globalRollbackOnly = status.isGlobalRollbackOnly();
                }
                if (status.hasSavepoint()) {
                    if (status.isDebug()) {
                        logger.debug("Releasing transaction savepoint");
                    }
                    status.releaseHeldSavepoint();
                }
                // 新事务才会执行doCommit方法
                else if (status.isNewTransaction()) {
                    if (status.isDebug()) {
                        logger.debug("Initiating transaction commit");
                    }
                    doCommit(status);
                }
                // Throw UnexpectedRollbackException if we have a global rollback-only
                // marker but still didn't get a corresponding exception from commit.
                if (globalRollbackOnly) {
                    throw new UnexpectedRollbackException(
                            "Transaction silently rolled back because it has been marked as rollback-only");
                }
            }
            ....

methodB率先执行commitTransactionAfterReturning方法,但是methodB并未开启新事务,是加入到methodA创建的事务的里,这样就无法执行doCommit方法。methodA再执行commitTransactionAfterReturning方法的时候,status.isNewTransaction()判断为真,执行doCommit方法,就将事务整体提交了。

新开线程调用

@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
methodA{
    ……
    new Thread(new Runnable() {
        public void run() {
            methodB();
        }
    }).start();
    localA();
    ……
}

@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
methodB{
   ……
}
场景 同一个类 非同一个类
localA异常 A回滚,B提交 A回滚,B提交
methodB捕获异常/无异常 提交 提交
methodB抛异常 提交 A提交,B回滚

这个场景的结果我们不用多分析了。既然事务信息是保存在ThreadLocal对象里,新开线程调用其它方法,不同线程维护各自的数据库连接,这必然无法保证事务一致。

新开事务

@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
methodA{
    ……
    methodB();
    localA();
    ……
}

@Transactional(propagation = Propagation.PROPAGATION_REQUIRES_NEW, rollbackFor = Exception.class)
methodB{
   ……
}
场景 同一个类 非同一个类
localA异常 回滚 A回滚,B提交
methodB捕获异常/无异常 提交 提交
methodB抛异常 回滚 回滚
同时更新同一条数据 methodA更新值为准 死锁

一、新开事务后,数据库连接是不是真的不一样?
我们来追踪下ThreadLocal对象中的值的变化。

(1) methodA

依次执行bindResourceregisterSessionHolder后,ThreadLocal对象中的值如下:

{org.apache.ibatis.session.defaults.DefaultSqlSessionFactory@6af9383b=org.mybatis.spring.SqlSessionHolder@35ccbc0b, {
    CreateTime:"2017-08-21 13:45:15",
    ActiveCount:1,
    PoolingCount:2,
    CreateCount:3,
    DestroyCount:0,
    CloseCount:0,
    ConnectCount:1,
    Connections:[
        {ID:1405383256, ConnectTime:"2017-08-21 13:45:15", UseCount:0, LastActiveTime:"2017-08-21 13:45:15"},
        {ID:1808673804, ConnectTime:"2017-08-21 13:45:15", UseCount:0, LastActiveTime:"2017-08-21 13:45:15"}
    ]
}=org.springframework.jdbc.datasource.ConnectionHolder@3f3d370a}

(2) methodB

由于methodB的传播性是PROPAGATION_REQUIRES_NEW,根据源码,其先后执行

    doGetTransaction
    suspend //挂起当前事务
    unbindResource //将methodA绑定在线程上数据库连接信息解绑,也就是resources.get()为null
    doBegin //新开启事务
    bindResource //将methodB的ConnectionHolder绑定在当前线程上
    registerSessionHolder //将methodB的SqlSessionHolder绑定在当前线程上

ThreadLocal对象中的值如下:

{org.apache.ibatis.session.defaults.DefaultSqlSessionFactory@6af9383b=org.mybatis.spring.SqlSessionHolder@334a8b18, {
    CreateTime:"2017-08-21 13:45:15",
    ActiveCount:2,
    PoolingCount:1,
    CreateCount:3,
    DestroyCount:0,
    CloseCount:0,
    ConnectCount:2,
    Connections:[
        {ID:1405383256, ConnectTime:"2017-08-21 13:45:15", UseCount:0, LastActiveTime:"2017-08-21 13:45:15"}
    ]
}=org.springframework.jdbc.datasource.ConnectionHolder@78c36125}

大家看,明显这两个方法的数据库连接是不一样的,比如ActiveCount、PoolingCount等。接下来,methodB将继续执行:

    commitTransactionAfterReturning //无异常,执行commitTransactionAfterReturning
    doCommit //提交当前事务
    cleanupAfterCompletion

methodB执行完毕,methodA继续执行,

    resume //还原methodA被挂起时的数据库连接信息
    bindResource //绑定到当前线程

ThreadLocal对象中的值如下:

{org.apache.ibatis.session.defaults.DefaultSqlSessionFactory@6af9383b=org.mybatis.spring.SqlSessionHolder@35ccbc0b, {
    CreateTime:"2017-08-21 13:45:15",
    ActiveCount:1,
    PoolingCount:2,
    CreateCount:3,
    DestroyCount:0,
    CloseCount:1,
    ConnectCount:2,
    Connections:[
        {ID:1405383256, ConnectTime:"2017-08-21 13:45:15", UseCount:0, LastActiveTime:"2017-08-21 13:45:15"},
        {ID:1808673804, ConnectTime:"2017-08-21 13:45:15", UseCount:1, LastActiveTime:"2017-08-21 13:56:24"}
    ]
}=org.springframework.jdbc.datasource.ConnectionHolder@3f3d370a}

最后,methodA执行commitTransactionAfterReturningdoCommit等方法,提交事务。

二、重点分析一下methodA调用非同一个类中的methodB方法,同时这两个方法会更新同一条数据的情况

methodB新开事务时,methodA对应的事务被挂起。新事务执行完毕才会执行老的事务。而oracle在更新数据时,会把该行记录锁住,老事务迟迟不提交,新事务又无法获得锁,因此一直等待。这就会出现死锁的情况。

不开事务的方法调用开事务的方法

以ClassA中不带事务注解的methodA调用ClassB中带事务注解的methodB、methodB抛出异常为例,

(1) methodA
这里写图片描述

(2) methodB
这里写图片描述

看起来methodA和methodB的数据库连接是一样的,其实是methodA提交完了事务,将数据库连接释放后、methodB又从数据库连接池取得了相同的那个数据库连接。但它们并不在同一个数据库连接里,不能保持事务统一。methodA提交完毕,methodB回滚。

展开阅读全文

没有更多推荐了,返回首页