前言
想的再多,不如行动起来,大家好,我是啊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的事务原理。