一文理解Spring事务传播属性

1. 了解事务传播属性

事务传播属性描述的是Spring当中一个事务方法中调用了别的事务方法时,应该怎么处理别的事务方法。最常见的有三个REQUIREDREQUIRES_NEWNESTED

  • 1.REQUIRED描述的是遇到这个事务方法,它将会共用调用方的事务。
  • 2.REQUIRES_NEW描述的是遇到这个事务方法时,新创建一个事务去执行。
  • 3.NESTED主要是涉及到Safepoint的概念,方法执行前添加Safepoint,方法执行后把Safepoint删掉,发生异常就回滚到Safepoint处。

具体的内容,在后续当中去进行体会,没有真实的案例比较难以说明。

我们有如下的两个Service类,XXXServiceUserService

@Service
public class XXXService {

    @Autowired
    UserService userService;

    @Transactional
    public void transaction() {
        userService.addUser();
        userService.updateUser();
    }
}
@Service
public class UserService {

    @Transactional
    public void addUser() {
        // doSomething
    }

    @Transactional
    public void updateUser() {
        // doSomething
    }
}

对于每个标注了@Transaction的方法,都会被Spring进行AOP动态代理,其中拦截该方法的执行用到的spring-tx包下的TransactionInterceptor这个事务拦截器所拦截到,去执行代理的逻辑。

对于每个@Transaction的方法,最终都会被类似如下的伪代码所调用(伪代码对于分析Spring事务传播属性非常有作用,对于写业务代码的分析当中简直是神器!一定要牢记)

createTransactionIfNecessary   // 如果必要的话,就创建一个新的事务
try {
    // 执行目标方法.......
} catch(Throwable ex) {
    // 回滚......
    throw ex;  // 把捕获的异常往上抛
}
commitTransactionAfterReturning // 提交事务

套用这个模板,对transaction方法的整个执行写出一个伪代码

createTransactionIfNecessary   // 如果必要的话,就创建一个新的事务
try {
    // 下面是addUser方法的被代理逻辑
    createTransactionIfNecessary   // 如果必要的话,就创建一个新的事务
    try {
        // 执行目标方法......
    } catch(Throwable ex) {
        // 回滚.......
        throw ex;  // 把捕获的异常往上抛
    }
    commitTransactionAfterReturning // 提交事务

    // 下面是updateUser方法的被代理逻辑
    createTransactionIfNecessary   // 如果必要的话,就创建一个新的事务
    try {
        // 执行目标方法......
    } catch(Throwable ex) {
        // 回滚.......
        throw ex;  // 把捕获的异常往上抛
    }
    commitTransactionAfterReturning // 提交事务
} catch(Throwable ex) {
    // 回滚.......
    throw ex;  // 把捕获的异常往上抛
}
commitTransactionAfterReturning // 提交事务

需要注意的是:

  • 1.并不是在每个代理方法执行完之后都有资格去提交/回滚事务的,只有1'该代理方法是创建事务的代理方法/2'该代理方法创建了Savepoint这两种情况下,才有资格去提交/回滚事务。这个点是非常重要的!
  • 2.对于REQUIREDREQUIRES_NEW当中,都没有涉及到创建Savepoint,对于创建Savepoint的情况,在后面讲述NESTED的情况下会进行进一步讲解,REQUIREDREQUIRES_NEW这两种情况下都是只有创建事务的目标代理方法才有资格去进行回滚/提交

2. 对于REQUIRED(默认)的情况进行分析

默认的事务传播属性为REQUIRED,代表了addUserupdateUser这两个事务方法,将会和transaction方法共用同一个事务,中途不会因为执行createTransactionIfNecessary的执行而去创建事务。

也就是说对于上面的三个事务方法而言,只有transaction方法是有资格去提交/回滚任务的,对于addUserupdateUser这两个方法都没有资格。

从上面的分析当中,我们可以知道在REQUIRED的情况下,addUserupdateUser既不能新创建事务,也不能新提交/回滚事务,因此我们将模板的调用关系进行简化得到下面的伪代码。

createTransactionIfNecessary   // 如果必要的话,就创建一个新的事务
try {
    // 下面是addUser方法的被代理逻辑
    try {
        // 执行目标方法......
    } catch(Throwable ex) {
        throw ex;  // 把捕获的异常往上抛
    }

    // 下面是updateUser方法的被代理逻辑
    try {
        // 执行目标方法......
    } catch(Throwable ex) {
        throw ex;  // 把捕获的异常往上抛
    }
} catch(Throwable ex) {
    // 回滚.......
    throw ex;  // 把捕获的异常往上抛
}
commitTransactionAfterReturning // 提交事务

对于全部采用默认(REQUIRED)的情况,下面模拟几种情况:

  • 1.如果三个方法都没抛出异常,那么事务执行到最后,因为transaction方法有资格提交事务,所以它将事务提交,然后调用结束。(结果:addUserupdateUser均提交成功)
  • 2.如果transaction方法执行过程中抛出了异常,那么直接在外层的catch代码块当中去进行回滚并将异常抛给处理transaction方法的上一级方法。(结果:addUserupdateUser要么没被执行,要么被回滚,最终一定都失败)
  • 3.如果addUser抛出了异常,那么直接就往上抛给transaction代理方法了(updateUser方法不会被执行到),被外层的transaction代理方法捕获到,因为transaction方法是有资格提交/回滚事务的,所以直接执行回滚,并且直接将异常抛给处理transaction方法的上一级方法。(结果:addUser被回滚,updateUser没机会被执行)
  • 4.如果updateUser抛出了异常,那么addUser方法是执行完了的,但是updateUser方法出了异常,还是抛给transaction代理方法了,因此也直接进行回滚,并且直接将异常抛给处理transaction方法的上一级方法。(结果:addUserupdateUser都被回滚)

还有个问题,有些业务代码直接整个try-catch代码块包住,比如如下代码

@Service
public class XXXService {

    @Autowired
    UserService userService;

    @Transactional
    public void transaction() {
        try {
        userService.addUser();
            userService.updateUser();
    } catch(Throwable ex) {
        ex.printStackTrace();
    }
    }
}

这种情况下,伪代码变成了如下

createTransactionIfNecessary   // 如果必要的话,就创建一个新的事务
try {
    try {
        // 下面是addUser方法的被代理逻辑
        try {
            // 执行目标方法......
        } catch(Throwable ex) {
            throw ex;  // 把捕获的异常往上抛
        }

        // 下面是updateUser方法的被代理逻辑
        try {
            // 执行目标方法......
        } catch(Throwable ex) {
            throw ex;  // 把捕获的异常往上抛
        }
    } catch(Throwable ex) {
        ex.printStackTrace();
    }
} catch(Throwable ex) {
    // 回滚.......
    throw ex;  // 把捕获的异常往上抛
}
commitTransactionAfterReturning // 提交事务

这种情况下,按照上面的分析,transaction的代理方法很明显在最外层捕获不到异常,因此就不会进行回滚,但是其实Spring这个框架考虑的很周到,只要内部抛出了异常,那么就会将一个标志位flag改为true,在执行transaction代理方法进行判断时,也会直接去进行回滚,而不是去提交事务。

还有另一种情况,在addUser/deleteUser当中对整个代码使用try-catch去进行包住了,那么Spring肯定没机会将flag改为true,最终肯定是不能对整个事务回滚,毕竟异常都被你抓了,Spring可感知不到抛了异常的情况呢!

所以我们可以总结一个点:transaction方法可以try-catch,也可以不try-catch,效果没什么不同,因为Spring都能感知到并进行处理。但是如果你在addUserupdateUser方法上try-catchSpring就感知不到异常的发生了,就需要根据你的业务来判断要不要try-catch

3. 对于REQUIRES_NEW的情况进行分析

我们将UserServiceaddUser方法的事务传播属性改为了REQUIRES_NEWREQUIRES_NEW代表的意思就是在执行这个方法的createTransactionIfNecessary方法时,会创建一个事务(并将之前的事务挂起),既然是它创建的事务,它就有资格去进行提交。

@Service
public class UserService {

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void addUser() {
        // doSomething
    }

    @Transactional
    public void updateUser() {
        // doSomething
    }
}

根据上面的分析,我们得到如下的伪代码

createTransactionIfNecessary   // 如果必要的话,就创建一个新的事务
try {
    // 下面是addUser方法的被代理逻辑
    createTransactionIfNecessary   // 如果必要的话,就创建一个新的事务
    try {
        // 执行目标方法......
    } catch(Throwable ex) {
        // 回滚.......
        throw ex;  // 把捕获的异常往上抛
    }
    commitTransactionAfterReturning // 提交事务

    // 下面是updateUser方法的被代理逻辑
    try {
        // 执行目标方法......
    } catch(Throwable ex) {
        throw ex;  // 把捕获的异常往上抛
    }
} catch(Throwable ex) {
    // 回滚.......
    throw ex;  // 把捕获的异常往上抛
}
commitTransactionAfterReturning // 提交事务

我们再来分析各种情况:

  • 1.如果各个方法都正常执行,addUser把自己的方法的事务提交了,transaction也把自己的事务提交了(它的事务当中没有addUser这个部分)。(结果:addUserupdateUser均成功提交)
  • 2.如果addUser方法出现异常(updateUser不会被执行),因为它(REQUIRES_NEW)有资格去回滚自己的事务,因此它先将自己的事务回滚了,然后再把异常抛给transaction代理方法,然后transaction代理方法捕获到了异常,因此它也将自己的事务进行了回滚,并且直接将异常抛给处理transaction方法的上一级方法。(结果:addUser被回滚,updateUser没执行)
  • 3.如果updateUser方法出现异常(addUser没出现异常,而它有资格提交自己的事务,因此addUser执行成功了!),和之前的情况一样,它没资格回滚事务,将异常抛给transaction代理方法,然后transaction代理方法捕获到了异常,将自己的事务进行了回滚,并且直接将异常抛给处理transaction方法的上一级方法。(结果:addUser提交成功,updateUser被回滚了)
  • 4.如果transaction方法在addUser之前就抛出了异常,那么addUser想新开事务也没机会,transaction整个事务直接回滚。(结果:addUserupdateUser都没机会执行)
  • 5.如果在transaction方法在addUser之后抛出了异常,那么addUser肯定将自己的事务提交了!但是transaction这个发生事务回滚了!(结果:addUser提交成功,addUser要么没执行,要么被回滚,反正最终是失败了)

4. 对于NESTED的情况进行分析

对于NESTED(翻译成中文叫嵌套),主要涉及到的内容有:

  • 1.对于NESTED的事务方法,不会创建一个新的事务,它不能执行提交事务,但是它能回滚事务到它自己设置的检查点(其实这是Safepoint的特点)。
  • 2.在NESTED的事务代理方法的执行时,会首先创建一个Safepoint,然后去执行目标方法。
    • 如果目标方法执行成功,那么会将刚才创建的Safepoint删掉,并将REQUIRED中提到的那个flag置为false(这个flag的点很关键)
    • 如果目标方法执行失败,将回滚到刚刚设置的Safepoint处去。

对于Safepoint的概念,其实在数据库层面(比如MySQL)上已经给我们进行了提供支持,在JDBC中也给我们提供了相关的支持。比如在如下的代码当中,设置了一个回滚点(Safepoint),执行了一堆操作之后,因为某些原因我突然想回滚到我刚刚设置到的回滚点处的情况去。

        Connection connection = DriverManager.getConnection("...");
        Savepoint savepoint = connection.setSavepoint("wanna");
        // doSomething...
        connection.rollback(savepoint);

我们将UserService中将两个方法的事务传播行为都改成NESTED

@Service
public class UserService {

    @Transactional(propagation = Propagation.NESTED)
    public void addUser() {
        // doSomething
    }

    @Transactional(propagation = Propagation.NESTED)
    public void updateUser() {
        // doSomething
    }
}

XXXService中的内容保持不变

@Service
public class XXXService {

    @Autowired
    UserService userService;

    @Transactional
    public void transaction() {
        userService.addUser();
        userService.updateUser();
    }
}

理想情况下的执行顺序如下:

1.在transaction的代理方法中开启事务
2.添加一个Safepoint(假设为sp1)
3.执行addUser方法
4.删除Safepoint(sp1)
5.添加一个Safepoint(假设为sp2)
6.执行updateUser方法
7.删除Safepoint(sp2)
8.提交事务......

我们来假想一种情况:假如addUser执行没抛异常,updateUser执行抛出了异常,我们使用Safepoint想要实现的目标就是updateUser给我回滚了,但是addUser能成功提交。

但是事实上执行第6步时抛出异常,回滚到sp2(updateUser回滚了)并且直接抛给transaction代理方法,transaction代理方法拿到这个异常,把整个事务给回滚了,最终addUser也被回滚了,很显然不符合我们想要实现的预期,但是Spring对这个点的实现其实很巧妙,它将flag修改为false了。

如果我们将整个方法块用try-catch包起来,并且不把异常抛给上级,因为flagfalse,直接就执行提交操作,但是这个过程中updateUser其实已经被回滚掉了,addUser则得到了提交。

@Service
public class XXXService {

    @Autowired
    UserService userService;

    @Transactional
    public void transaction() {
        try {
            userService.addUser();
            userService.updateUser();
        } catch (Throwable ex) {
            ex.printStackTrace();
        }

    }
}

所以我们可以总结一个点:在使用NESTED时,要想实现我们的目标效果,需要对整个方法进行try-catch进行捕获,别让Spring捕获到异常。

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

推荐阅读更多精彩内容