一个dubbo异常引发的事务血案

912 阅读8分钟

前言

想的再多,不如行动起来,大家好,我是啊Q,让我们徜徉在知识的海洋里吧。

一起“开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第9天, 点击查看活动详情

现场描述

情况描述:如下图的伪代码。 dubboService.do() 是一个dubbo服务调用,在调用时dubbo抛出了一个RPC的异常,该调用我已经处理异常了,但是 doSubmit() 方法执行保存数据逻辑的方法依然被回滚了。这是怎么回事呢?今天我们来分析一下。

AService.A(){
    doSubmit(); // 执行保存数据逻辑
    ...
    BService.do(); //  执行调用BService的方法
        ...
}

BService.do(){
    // 远程dubbo调用报错 RpcException
    try{dubboService.do();}catch(Throwable e){...}  
       }

事务相关概念

Ⅰ:事务特性(ACID)

  • 1.Atomic(原子性)  原子性,数据操作的最小单元是事务,而不是sql语句
  • 2.Consistency(一致性) 事务完成前后,数据要保持逻辑的一致性
  • 3.Isolation(一致性) 一个事务执行的过程中,不应该受到其他事务的干扰
  • 4.Durability(持久性) 事务一旦结束,数据就持久到数据库

Ⅱ:传播机制(Propagation)

  • required : 如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是最常见的选择。
  • supports : 支持当前事务,如果当前没有事务,就以非事务方式执行
  • mandatory : 使用当前的事务,如果当前没有事务,就抛出异常
  • requires_new : 新建事务,如果当前存在事务,把当前事务挂起
  • not_supported : 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起
  • never : 以非事务方式执行,如果当前存在事务,则抛出异常
  • nested : 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。

III:事务的隔离级别

  • 1.READ_UNCOMMITTED 读取未提交数据(会出现脏读,不可重复读): 一个事务可以读取另一个未提交事务的数据  读到了还未提交的数据就是脏读
  • 2.READ_COMMITTED 读取已提交数据(会出现不可重复读和幻读):  一个事物范围内两个相同的查询却返回了不同数据,这就是不可重复读
  • 3.REPEATABLE_READ  可重复读(会出现幻读) 可重复读意义就是该事务无论都多少次该数据,该数据都返回一致。  幻读针对的insert操作
  • 4.SERIALIZABLE 串行化 效率低

spring-tx事务测试

场景1:方法外调用测试(cglib代理)

test(){
    testService.test1( new Object[][]{{"lisi", 1}, {"lisi1", 2}} );
    testService.test2( new Object[]{"lisi1", 2} );
}        
@Transactional(propagation = Propagation.REQUIRED)
    public void test1(Object[][] params) {
    jdbcTemplate.update( slq, params[0] );  // sql1
}
@Transactional(propagation = Propagation.REQUIRED)
    public void test2(Object[] params) {
    jdbcTemplate.update( slq, params ); // sql2
    int a = 1 / 0; // 子方法抛出异常。
}

结果说明

  • 1.这种情况相当于两个方法分别执行,互不相干。相当于两条平行线,永不相交。
  • 2.这种情况和我们的Controller调用多个service是一个意思,就是test1和test2开启的事务是两个事务,两者方法的执行互不影响。
  • 3.MANDATORY全红的意义是:在事务开始时,没找到当前事务,事务管理器直接抛出异常,方法结束,所以两个方法根本还没来的急执行。

场景2:方法内调用测试(cglib代理)

testService.test1( new Object[][]{{"lisi", 1}, {"lisi1", 2}} );

@Override
    @Transactional(propagation = Propagation.REQUIRED)
    public void test1(Object[][] params) {
    jdbcTemplate.update( slq, params[0] );  // sql1
    test2( params[1] );
}
public void test2(Object[] params) {
    jdbcTemplate.update( slq, params ); // sql2
    int a = 1 / 0; // 子方法抛出异常。
}

结果说明

  • 1.这种情况相当于两个方法在同一个盒子里,一荣俱荣,一损俱损。相当于一条线,没有其他路可走。
  • 2.这种情况和我们的service里面调用方法是一样的,其实从结果图中我们可以得出结论:test1和test2的两个sql其实等价于test1中直接执行两个sql 。
public void test1(Object[][] params) {
    jdbcTemplate.update( slq, params[0] );  // sql1
    jdbcTemplate.update( slq, params[1] );  // sql2
}
  • 3.从图中我们可以看出,在test2加上事务注解也是无效的,这是为什么呢?
  • 4.MANDATORY全红的意义和场景一是一样的。

场景3***:方法内调用外部方法测试(cglib代理)

testService.test5( new Object[][]{{"lisi", 1}, {"lisi1", 2}} );

@Override
    @Transactional(propagation = Propagation.NESTED
                  )
    public void test5(Object[][] params) {
    jdbcTemplate.update( slq, params[0] );  // sql1
    testService2.rTest( params[1] ); // 调用rTest
}
@Override
    @Transactional(propagation = Propagation.NESTED
                  )
    public void rTest(Object[] params) {
    jdbcTemplate.update(slq, params);  // sql2
    int a = 1 / 0;
}

结果说明

  • 1.这种情况情况比较多,需要具体问题具体分析。

场景4***:语义重现

  • 1.requires_new
testService.test5();

@Transactional(propagation = Propagation.REQUIRES_NEW)
    public void test5(Object[][] params) {
    jdbcTemplate.update(slq, new Object[]{"lisi1" , "1"});  // sql1
    testService2.test2(); // 调用rTest
    testService2.test3(); // 调用nTest
}

@Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void test2() {
    jdbcTemplate.update(slq, new Object[]{"lisi2", "2"});  // sql2
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
    public void test3() {
    jdbcTemplate.update(slq, new Object[]{"lisi3", "3"});  // sql3
    int a = 1 / 0;
}

结果:sql1失败 sql2成功 sql3失败
  • 2.nested
testService.test5();
public void test5() {
    try{
        jdbcTemplate.update(slq, new Object[]{"lisi1", "1"});  // sql1
        testService2.test2(); // 调用rTest
        testService2.test3(); // 调用nTest
    }catch(Exception e){
        e.printStackTrace();
    }
}
@Transactional(propagation = Propagation.NESTED)
    public void test2() {
    jdbcTemplate.update(slq, new Object[]{"lisi2", "2"});  // sql2
}

@Transactional(propagation = Propagation.NESTED)
    public void test3() {
    jdbcTemplate.update(slq, new Object[]{"lisi3", "3"});  // sql3
    int a = 1 / 0;
}

该示例演示了nested嵌套事务的语义,当test3报错跑到上层并拦截异常处理后,事务异常只会回滚sql3。

思考

  • 1.为什么场景1中的service执行互不影响?
  • 2.为什么场景2中的test2方法上的注解无效?
  • 3.为什么场景3中的(REQUIRED,--) 会都失败呢(回滚了)?
  • 4.为什么场景3中的(REQUIRED,REQUIRES_NEW) 会都失败呢(回滚了)?
  • 5.想一想我们在场景3中的 int a = 1 / 0; 加上异常处理后结果又会是啥?

思考回答

  • 1.那是因为两个入口方法都是通过代理类进行调用的,两个方法都走了代理过程。
  • 2.那是因为入口方法test1在代理时生成代理类后,直接调用的代理类内部的test2方法,test2方法根本没有被代理。
    我们可以通过java的HSDB工具生成代理类来观察:java -classpath "%JAVA_HOME%/lib/sa-jdi.jar" sun.jvm.hotspot.HSDB
  • 3.因为TestService2Impl中的rTest()没有加任何事务机制,就造成了spring也不会对其生成代理类。
    不生成代理类,就相当于普通方法调用,两个方法其实处在同一个事务中。
  • 4.不要被REQUIRES_NEW迷惑了这里,这里需要rTest()重启开了一个事务,
    但是异常还是被test5方法捕获到了,所以两个方法的事务都回滚了 。
  • 5.看懂了3,4的说法,5应该就能理解了。

总结(**)

  • 1.如果方法调用是同类中的方法调用,则事务的传递属性以开始方法为准,其他方法上的事务将无效。
  • 2.如果外部方法不存在事务,则事务以各自的方法事务为准,互不干扰。
  • 3.如果外部存在事务,则以外部事务为准,当内部事务异常传递到外部方法时,将导致外部事务回滚。
    比如(supports,requires_new,requires_new),当事务3抛出异常后,事务1在没有事务的情况下,即使捕获了事务3的异常,事务1也不会回滚;
    比如(required,requires_new,requires_new),当事务3抛出异常后,事务1在有事务的情况下,其捕获了事务3的异常,事务1跟着回滚。
  • 4.如果外部存在事务,内部各自方法的各自事务互不影响。
    比如(required,requires_new,requires_new),当事务3抛出异常后,事务2在新创建的独立事务下执行,无法捕获到事务3的异常,所以也不会回滚;
    比如(required,required,requires_new),当事务3抛出异常后,事务2在和事务1同一个事务中执行时,事务1捕获到异常,所以事务1,2都会回滚。
  • 5.如果外部存在事务,外部事务无法影响内部事务
    比如(required,requires_new,requires_new),当事务1抛出异常后,事务2,3在新创建的独立事务下执行,无法捕获到事务1的异常,所以2,3不会回滚;
  • 6.特别的嵌套事务,当有嵌套事务时,其和他有关联的事务都会被该事务影响(基于savePoint)[savepoint]。
    当事务异常被捕获后,将基于保存点回滚事务(参考场景4-2)
    比如(required,NESTED,NESTED),当事务3抛出异常后,事务1,2都被嵌套,3着事务都会被回滚。
    比如(required,requires_new,NESTED),当事务3抛出异常后,事务1,3被回滚,2正常提交。
  • 7.@Transactional注解可以标记于接口方法上。

后记

基于上诉分析,我们来分析一下我们开始遇到的那个问题:

原因是:是因为xml配置中 <aop:pointcut id="txPointcut1" expression="execution( com.arm..service.. .*(..))" /> 。
我们在切面的时候切到了远程dubbo服务的service接口,spring通过dubbo生成的referenceBean(调用bean)和接口类生成了事务切面,dubboService 外部被包装了两个代理类,referenceBean和事物代理类,

而我们拦截的其实只是 dubboService这个类。并没有拦截referenceBean这个代理类,这个代理中执行了RPC的远程调用而抛出异常,造成了事务回滚。
其实说白了就是 dubboService.do(); 这句代码还没真正的执行就抛出了异常。

tips:但是这千万不能理解为这是分布式事务的体现,因为如果有多个远程调用的话,很明显这个dubbo异常只会对调用的主程序造成事务的回滚。

明天我们接着分析一下spring的事务原理。