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<TransactionSynchronization> 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。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,287评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,346评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,277评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,132评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,147评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,106评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,019评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,862评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,301评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,521评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,682评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,405评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,996评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,651评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,803评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,674评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,563评论 2 352

推荐阅读更多精彩内容