spring事务管理

Spring的事务管理是非常重要的一个内容和环节,我们特此记录下:

事务传播行为(Propagation)

所谓事务的传播行为是指,如果在开始当前事务之前,一个事务上下文已经存在,此时有若干选项可以指定一个事务性方法的执行行为。在TransactionDefinition定义中包括了如下几个表示传播行为的常量:

  • TransactionDefinition.PROPAGATION_REQUIRED:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。这个也是默认的传播行为
  • TransactionDefinition.PROPAGATION_REQUIRES_NEW:创建一个新的事务,如果当前存在事务,则把当前事务挂起。也就是两个事务是不同的上下文。
  • TransactionDefinition.PROPAGATION_SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
  • TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。
  • TransactionDefinition.PROPAGATION_NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。
  • TransactionDefinition.PROPAGATION_NESTED:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;则新建一个事务。

事务传播行为的几点理解

事务传播行为在事务的处理上是一个经常容易混淆但是却非常重要的一个环节。在下面我们进行相应的讨论:
首先需要明确的一点是,当我们在讨论事务的时候,很难和他的底层实现区分开,去看源码是最方便的,但是也是最痛苦的一点。所以为了使大家能够较清晰的理解,我们需要知道三点:一是通过AOP来实现,二是通过Exception来捕获回滚的标志位。三是与数据库的链接Connection是通过线程ThreadLocal类来与线程保持关联,从而实现在同一个线程中能够进行回滚的操作。
为了方便讨论,我们相应的列举出两种情况:


image.png
  • 情况一:方法A中调用方法B
  • 情况二:方法A中分别调用方法B和方法C

我们假设有一个如下的数据库:

create table tb_person(
  id bigint(20) auto_increment,
  name varchar(100) not null,
  primary key (id)
)

表中现有数据:

id name
1 赵四

1. REQUIRED传播行为的几种情况

REQUIRED的关键是使用同一个TX,也就是事务上下文。没有的话就会重建一个。同一个上下文的意思就是,如果进行COMMIT的话是统一进行的,也就是最外层的服务A完成时会调用COMMIT来进行提交,由于是一个TX,所以要发生回滚,比如C中发生异常,则是A, B, C均进行回滚的。正如我们在开头所明确的一点,事务管理是通过Exception来进行回滚的,那么有个很有趣的情况是,如果方法C中产生了异常,而在外层方法A中我们对异常进行了捕获,那么会有什么情况?我们先给出答案然后在进行实验来进行证明:A,B,C全部会回滚。
实验如下:

Service("AService")
public class AService {

    @Resource
    private Person personDao;

    @Resource
    private BService bService;

    @Transactional
    public void methodA() {
        Person person = auditDao.findById(1L);
        audit.setName("张三");
        personDao.updateSelective(person);
        try {
            bService.methodB();
            bService.methodC();
        } catch (Exception e) {
        }
    }

    public static void main(String[] args) {
        ApplicationContext ac = new ClassPathXmlApplicationContext("conf/applicationContext.xml");
        System.out.println("start applicationContext");
        ac.getBean("AService", AService.class).methodA();
    }
}

@Service("BService")
public class BService {

    @Resource
    private PersonDao personDao;

    @Transactional(propagation = Propagation.REQUIRED)
    public void methodB() {
            Person person = personDao.findById(1L);
            person.setName("李四");
            personDao.updateSelective(person);
    }

    @Transactional(propagation = Propagation.REQUIRED)
    public void methodC() {
        Person person = personDao.findById(1L);
        person.setName("王五");
        personDao.updateSelective(person);
        throw RuntimeException("[BService] method C");
    }
}

当运行后,发生回滚,表中数据没有发生改变。不过比较有趣的现象是,此时会抛出一个错误:

Exception in thread "main" org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
at org.springframework.transaction.support..commit(AbstractPlatformTransactionManager.java:724)
    at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:504)
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:292)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96)
...

如果我们理解之前所说的Spring的事务是依赖异常来进行处理的就能够明白这个的含义。我们看下源码:

Class TransactionAspectSupport
...
if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
            // Standard transaction demarcation with getTransaction and commit/rollback calls.
            TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
            Object retVal = null;
            try {
                // This is an around advice: Invoke the next interceptor in the chain.
                // This will normally result in a target object being invoked.
                retVal = invocation.proceedWithInvocation();
            }
            catch (Throwable ex) {
                // target invocation exception
                completeTransactionAfterThrowing(txInfo, ex);
                throw ex;
            }
            finally {
                cleanupTransactionInfo(txInfo);
            }
            commitTransactionAfterReturning(txInfo);
            return retVal;
        }
...

实际在C中抛出异常时,会被以上的代码段catch住异常并重新抛出,在这步操作之间,会执行completeTransaction(),此方法会根据PROPAGATION进行不同的回滚操作,比如REQUIRED的话就不回滚,而是直接通过。此时会设置回滚的标志位ResourceHolder的rollbackOnly位true。而我们在A方法中虽然捕获了异常,但是由于回滚标志位rollbackOnly仍然为true,所以在最外层的A方法的commit的时候,仍然会进行回滚,并将异常抛出。具体代码:

@Override
    public final void commit(TransactionStatus status) throws TransactionException {
        if (status.isCompleted()) {
            throw new IllegalTransactionStateException(
                    "Transaction is already completed - do not call commit or rollback more than once per transaction");
        }

        DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status;
        if (defStatus.isLocalRollbackOnly()) {
            if (defStatus.isDebug()) {
                logger.debug("Transactional code has requested rollback");
            }
            processRollback(defStatus);
            return;
        }
        if (!shouldCommitOnGlobalRollbackOnly() && defStatus.isGlobalRollbackOnly()) {
            if (defStatus.isDebug()) {
                logger.debug("Global transaction is marked as rollback-only but transactional code requested commit");
            }
            processRollback(defStatus);
            // Throw UnexpectedRollbackException only at outermost transaction boundary
            // or if explicitly asked to.
            if (status.isNewTransaction() || isFailEarlyOnGlobalRollbackOnly()) {
                throw new UnexpectedRollbackException(
                        "Transaction rolled back because it has been marked as rollback-only");
            }
            return;
        }

        processCommit(defStatus);
    }

其中,我们进行status.isNewTransaction()判断是不是最外层,也就是A。

2. REQUIRED_NEW和REQUIRE的区别

从描述来看,REQUIRED_NEW是创建一个新的事务,并将当前事务挂起,是两个完全不同的事务上下文。比如情况一,方法B的传播级别为REQUIRDED_NEW,则B是否回滚是由B是否抛出异常决定,事务A是否回滚是由A自己是否抛出异常决定。B的COMMIT发生在B的结束,A的COMMIT发生在A的结束。

3. REQUIRED_NEW和NESTED的区别

stackoverflow有一篇文章描述的较好:
differences-between-requires-new-and-nested-propagation-in-spring-transactions
我们引用其内容:

PROPAGATION_REQUIRES_NEW starts a new, independent "inner" transaction for the given scope. This transaction will be committed or rolled back completely independent from the outer transaction, having its own isolation scope, its own set of locks, etc. The outer transaction will get suspended at the beginning of the inner one, and resumed once the inner one has completed. ...
PROPAGATION_NESTED on the other hand starts a "nested" transaction, which is a true subtransaction of the existing one. What will happen is that a savepoint will be taken at the start of the nested transaction. Íf the nested transaction fails, we will roll back to that savepoint. The nested transaction is part of of the outer transaction, so it will only be committed at the end of of the outer transaction. ...

总的来说,就是NESTED是创建一个子事务,而REQUIRED_NEW是重建一个新事务。根据数据库的事务隔离我们知道,REQUIRED_NEW的话我们所看到的视图是和NESTED不同的。比如在情况一中,如果我们在方法A中更新了person的名称为张三,而B我们使用的是REQUIRED_NEW,则在方法B中我们看到的仍为数据库原始的名称,而在NESTED的话则我们看到的person名称为张三。还有一个是NESTED的提交是依赖于父事务的,也就是只有A COMMIT时B中的改动才能被提交。

3. REQUIRED和NESTED的区别:

从上面的分析中,是否感觉REQUIRED和NESTED很像呢。其中的区别是REQUIRED由于使用的是同一个TX,所以如果B中抛出了异常,则无论A中如何进行操作,A在COMMIT时均会回滚。而对于NESTED来讲,在从A中进入B中时,会记一个savepoint,而B发生异常后,B会回滚到savepoit。如果A中对B的异常进行了捕获,则A并不会发生回滚。这是与REQUIRED的主要区别。

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

推荐阅读更多精彩内容