Spring中事务传播级别的理解

Propagation是@Transactional注解的参数,定义了Spring在执行事务方法时处理事务的策略。有一下几个枚举值:

  • REQUIRED
    方法需要在事务中执行,如果已经有事务,则在此事务中执行,否则,新建事务
  • SUPPORTS
    方法支持在事务中执行,但是如果现在没有事务,则以非事务的方式执行
  • MANDATORY
    方法需要在事务中执行,若没有事务,则抛异常
  • REQUIRES_NEW
    方法需要在新事务中执行,若当前没有事务,则开启事务执行;若已有事务,则将它挂起,开启新事务执行
  • NOT_SUPPORTED
    方法不支持事务,若当前有事务,则将它挂起,以非事务的方式运行
  • NEVER
    方法不支持事务,若当前有事务,则抛出异常
  • NESTED
    方法支持嵌套事务,若当前有事务,则以嵌套事务的方式执行,否则,开启事务执行

Demo测试

其中SUPPORTS、MANDATORY、NOT_SUPPORTED、NEVER最多只有一个事务在执行,也比较好理解,就不多研究了。
REQUIRED、REQUIRES_NEW、NESTED有点不好理解,主要是“挂起”、“嵌套”这些术语的意义不是很清楚。
下面先通过一个demo来测试下这几个传播级别的差别,对内外层方法对影响。
首先定义了两个service,外层的UserService会执行向db插入一条数据的操作,然后会调用内层的CommonService,CommonService中方法模拟异常抛出回滚。

接口:
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
public interface IUserService {
    String saveUser(User user);
}

@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public interface ICommonService {
    void testException();
}

实现:
@Service
public class UserService implements IUserService {
    @Autowired
    UserDao mUserDao;
    @Autowired
    ICommonService mCommonService;
    @Override
    public String saveUser(User user) {
        mUserDao.insertUser(user);
        try {
            mCommonService.testException();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "Success";
    }
}

@Service
public class CommonService implements ICommonService {
@Override
    public void testException() {
        mCommonDao.updateRecord();
        int i = 1/0;
    }
}

外层将传播级别固定设为Propagation.REQUIRED,以开启事务,内层方法的传播级别分别设为Propagation.REQUIRED、REQUIRES_NEW、NESTED来进行测试。

  • 当内层方法设置为Propagation.REQUIRED时,发现执行失败,抛异常:
    org.springframework.transaction.UnexpectedRollbackException:
    Transaction rolled back because it has been marked as rollback-only
    外层事务回滚了,数据插入失败。虽然内层的异常在外层被捕获了,但是由于内层也经过了事务逻辑的增强,在异常的时候设置了回滚,在外层事务逻辑提交的时候,发现同一个事务已经被标记为“rollback-only”,所以也只能抛出异常了。
  • 当内层方法设置为Propagation. REQUIRES_NEW时,发现执行成功,外层数据插入逻辑成功,内层方法更新失败,互相没有影响,说明是在两个事务中执行的。
  • 当内层方法设置为Propagation. NESTED时,发现也是执行成功,外层数据插入逻辑成功,内层方法更新失败,和Propagation. REQUIRES_NEW的现象一样,那么它们有什么差别呢。

前面说到了挂起、嵌套,Propagation. REQUIRES_NEW应该是挂起了之前的事务,新创建了事务,而Propagation. NESTED是一个嵌套的事务。刚看到这些术语的时候我以为是rdbs中的定义,后来查询半天并没有找到,想到应该是spring中做的处理,于是研究下源码。

源码分析

我们知道spring中声明式事务的切面逻辑是在TransactionInterceptor中执行的,终点看下这个类里面的源码在已有事务的情况下,子方法设置为REQUIRED、REQUIRES_NEW、NESTED的处理逻辑:

@Override
    @Nullable
    public Object invoke(MethodInvocation invocation) throws Throwable {
        // Work out the target class: may be {@code null}.
        // The TransactionAttributeSource should be passed the target class
        // as well as the method, which may be from an interface.
        Class targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);

        // Adapt to TransactionAspectSupport's invokeWithinTransaction...
        return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed);
    }

跟进代码,发现创建事务的逻辑在

@Override
    public final TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
            throws TransactionException {

        // Use defaults if no transaction definition given.
        TransactionDefinition def = (definition != null ? definition : TransactionDefinition.withDefaults());

        Object transaction = doGetTransaction();
        boolean debugEnabled = logger.isDebugEnabled();

        if (isExistingTransaction(transaction)) {
            // Existing transaction found -> check propagation behavior to find out how to behave.
            return handleExistingTransaction(def, transaction, debugEnabled);
        }

若当前已有事务,则执行handleExistingTransaction的逻辑

if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW) {
            if (debugEnabled) {
                logger.debug("Suspending current transaction, creating new transaction with name [" +
                        definition.getName() + "]");
            }
            SuspendedResourcesHolder suspendedResources = suspend(transaction);
            try {
                boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
                DefaultTransactionStatus status = newTransactionStatus(
                        definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);
                doBegin(transaction, definition);
                prepareSynchronization(status, definition);
                return status;
            }
            catch (RuntimeException | Error beginEx) {
                resumeAfterBeginException(transaction, suspendedResources, beginEx);
                throw beginEx;
            }
        }

        if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {
            if (!isNestedTransactionAllowed()) {
                throw new NestedTransactionNotSupportedException(
                        "Transaction manager does not allow nested transactions by default - " +
                        "specify 'nestedTransactionAllowed' property with value 'true'");
            }
            if (debugEnabled) {
                logger.debug("Creating nested transaction with name [" + definition.getName() + "]");
            }
            if (useSavepointForNestedTransaction()) {
                // Create savepoint within existing Spring-managed transaction,
                // through the SavepointManager API implemented by TransactionStatus.
                // Usually uses JDBC 3.0 savepoints. Never activates Spring synchronization.
                DefaultTransactionStatus status =
                        prepareTransactionStatus(definition, transaction, false, false, debugEnabled, null);
                status.createAndHoldSavepoint();
                return status;
            }
            else {
                // Nested transaction through nested begin and commit/rollback calls.
                // Usually only for JTA: Spring synchronization might get activated here
                // in case of a pre-existing JTA transaction.
                boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
                DefaultTransactionStatus status = newTransactionStatus(
                        definition, transaction, true, newSynchronization, debugEnabled, null);
                doBegin(transaction, definition);
                prepareSynchronization(status, definition);
                return status;
            }
        }

可以看到,处理PROPAGATION_NESTED和PROPAGATION_REQUIRES_NEW处理的不同,PROPAGATION_NESTED是suspend了老事务,重新开启了新事务,而PROPAGATION_NESTED是保存点savepoint,这下比较清楚了,嵌套事务是通过savepoint来实现的,外层事务设置保存点,内存方法回滚的时候是回滚到外层的保存点,外层事务的执行不受影响,但是由于还是一个事务,所以事务的提交肯定是由外层来进行的,所以外层事务异常回滚的话,内存逻辑肯定也会回滚。可以修改demo测试一下,将异常代码移到外层逻辑,发现果然如此。而PROPAGATION_REQUIRES_NEW却不受影响,可见果然是两个分开的事务,那么是怎么实现的呢?
查看suspend和doBegin的代码

protected final SuspendedResourcesHolder suspend(@Nullable Object transaction) throws TransactionException {
        if (TransactionSynchronizationManager.isSynchronizationActive()) {
            List suspendedSynchronizations = doSuspendSynchronization();
            try {
                Object suspendedResources = null;
                if (transaction != null) {
                    suspendedResources = doSuspend(transaction);
                }
                String name = TransactionSynchronizationManager.getCurrentTransactionName();
                TransactionSynchronizationManager.setCurrentTransactionName(null);
                boolean readOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly();
                TransactionSynchronizationManager.setCurrentTransactionReadOnly(false);
                Integer isolationLevel = TransactionSynchronizationManager.getCurrentTransactionIsolationLevel();
                TransactionSynchronizationManager.setCurrentTransactionIsolationLevel(null);
                boolean wasActive = TransactionSynchronizationManager.isActualTransactionActive();
                TransactionSynchronizationManager.setActualTransactionActive(false);
                return new SuspendedResourcesHolder(
                        suspendedResources, suspendedSynchronizations, name, readOnly, isolationLevel, wasActive);
            }
            catch (RuntimeException | Error ex) {
                // doSuspend failed - original transaction is still active...
                doResumeSynchronization(suspendedSynchronizations);
                throw ex;
            }
        }
        else if (transaction != null) {
            // Transaction active but no synchronization active.
            Object suspendedResources = doSuspend(transaction);
            return new SuspendedResourcesHolder(suspendedResources);
        }
        else {
            // Neither transaction nor synchronization active.
            return null;
        }
    }

发现在挂起事务时,将当前的事务信息从TransactionSynchronizationManager中移除了,同时清除的还有transaction中的连接信息,这样在再次开始事务时获取到的是一个新的连接

@Override
    protected void doBegin(Object transaction, TransactionDefinition definition) {
        DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
        Connection con = null;

        try {
            if (!txObject.hasConnectionHolder() ||
                    txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
                Connection newCon = obtainDataSource().getConnection();
                if (logger.isDebugEnabled()) {
                    logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction");
                }
                txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
            }

内外层执行用的都不是一个数据库连接,那自然也就不是一个事务了。
总结:spring中事务的挂起、嵌套不是数据库系统中定义的概念,事务的挂起其实就是将原连接进行保存,用新的连接去处理内层事务,处理完成再用保存的连接继续执行之前的事务;而嵌套事务对应的是数据库系统的savepoint,嵌套一个内事务时时间上是设置了一个保存点,内存逻辑回滚只回滚到保存点,不影响外层逻辑,由于还是一个事务,所以外层逻辑如果回滚了,内层逻辑依然会被回滚。如果需要两个事务逻辑互不影响,请使用REQUIRES_NEW。

你可能感兴趣的:(Spring中事务传播级别的理解)