龙空技术网

完蛋,我的事务怎么不生效?

KK星牛通 450

前言:

此刻你们对“mysql事务超时自动回滚”大体比较关切,同学们都需要分析一些“mysql事务超时自动回滚”的相关文章。那么小编同时在网络上网罗了一些关于“mysql事务超时自动回滚””的相关知识,希望你们能喜欢,你们快快来了解一下吧!

前言

推荐阅读:亿人血书!阿里云内部独家的K8s+Docker套餐,有内味了

事务大家平时应该都有写,之前写事务的时候遇到一点坑,居然不生效,后来排查了一下,复习了一下各种事务失效的场景,想着不如来一个总结,这样下次排查问题,就能有恃无恐了。那么先来复习一下事务相关知识,事务是指操作的最小工作单位,作为一个单独且不可切割的单元操作,要么全部成功,要么全部失败。事务有四大特性(ACID):

原子性(Atomicity):事务包含的操作,要么全部成功,要么全部失败回滚,不会存在一半成功一半失败的中间状态。比如AB一开始都有500元,AB转账100,那么A的钱少了100B的钱就必须多了100,不能A少了钱,B也没收到钱,那这个钱就不翼而飞了,不符合原子性了。一致性(Consistency):一致性是指事务执行之前和之后,保持整体状态的一致,比如AB一开始都有500元,加起来是1000元,这个是之前的状态,AB转账100,那么最后A400B600,两者加起来还是1000,这个整体状态需要保证。隔离性(Isolation):前面两个特性都是针对同一个事务的,而隔离性指的是不同的事务,当多个事务同时在操作同一个数据的时候,需要隔离不同事务之间的影响,并发执行的事务之间不能相互干扰。持久性(Durability):指事务如果一旦被提交了,那么对数据库的修改就是永久性的,就算是数据库发生故障了,已经发生的修改也必然存在。

事务的几个特性并不是数据库事务专属的,广义上的事务是一种工作机制,是并发控制的基本单位,保证操作的结果,还会包括分布式事务之类的,但是一般我们谈论事务,不特指的话,说的就是与数据库相关的,因为我们平时说的事务基本都基于数据库来完成。

事务不仅是适用于数据库。我们可以将此概念扩展到其他组件,类似队列服务或外部系统状态。因此,“一系列数据操作语句必须完全完成或完全失败,以一致的状态离开系统”

测试环境

前面我们已经部署过了一些demo项目,以及用docker快速搭建环境,本文基于的也是之前的环境:

JDK 1.8Maven 3.6DockerMysql事务正常回滚的样例

正常的事务样例,包含两个接口,一个是获取所有的用户中的数据,另外一个更新的,是update用户数据,其实就是每个用户的年龄+1,我们让一次操作完第一个之后,抛出异常,看看最后的结果:

@Service("userService")public class UserServiceImpl implements UserService {    @Resource    UserMapper userMapper;    @Autowired    RedisUtil redisUtil;    @Override    public List<User> getAllUsers() {        List<User> users = userMapper.getAllUsers();        return users;    }    @Override    @Transactional    public void updateUserAge() {        userMapper.updateUserAge(1);        int i= 1/0;        userMapper.updateUserAge(2);    }} 

数据库操作:

<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "; ><mapper namespace="com.aphysia.springdocker.mapper.UserMapper">    <select id="getAllUsers" resultType="com.aphysia.springdocker.model.User">        SELECT * FROM user    </select>    <update id="updateUserAge" parameterType="java.lang.Integer">        update user set age=age+1 where id =#{id}    </update></mapper></code-pre> 

先获取所有的用户看看:

在调用更新接口,页面抛出错误了:

控制台也出现了异常,意思是除以0,异常:

java.lang.ArithmeticException: / by zero    at com.aphysia.springdocker.service.impl.UserServiceImpl.updateUserAge(UserServiceImpl.java:35) ~[classes/:na]    at com.aphysia.springdocker.service.impl.UserServiceImpl$FastClassBySpringCGLIB$c8cc4526.invoke(<generated>) ~[classes/:na]    at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218) ~[spring-core-5.3.12.jar:5.3.12]    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:783) ~[spring-aop-5.3.12.jar:5.3.12]    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) ~[spring-aop-5.3.12.jar:5.3.12]    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:753) ~[spring-aop-5.3.12.jar:5.3.12]    at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:123) ~[spring-tx-5.3.12.jar:5.3.12]    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:388) ~[spring-tx-5.3.12.jar:5.3.12]    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119) ~[spring-tx-5.3.12.jar:5.3.12]    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.12.jar:5.3.12]    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:753) ~[spring-aop-5.3.12.jar:5.3.12]    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:698) ~[spring-aop-5.3.12.jar:5.3.12]    at com.aphysia.springdocker.service.impl.UserServiceImpl$EnhancerBySpringCGLIB$25070cf0.updateUserAge(<generated>) ~[classes/:na]</code-pre> 

然后我们再次请求,看到数据两个都是11说明数据都没有发生变化,第一个操作完之后,异常,回滚成功了:

[{"id":1,"name":"李四","age":11},{"id":2,"name":"王五","age":11}]</code-pre>

那什么时候事务不正常回滚呢?且听我细细道来:

实验1. 引擎设置不对

我们知道,Mysql其实有一个数据库引擎的概念,我们可以用show engines来查看Mysql支持的数据引擎:

可以看到Transactions那一列,也就是事务支持,只有InnoDB,那就是只有InnoDB支持事务,所以要是引擎设置成其他的事务会无效。

我们可以用show variables like 'default_storage_engine'看默认的数据库引擎,可以看到默认是InnoDB:

mysql> show variables like 'default_storage_engine';+------------------------+--------+| Variable_name          | Value  |+------------------------+--------+| default_storage_engine | InnoDB |+------------------------+--------+</code-pre> 

那我们看看我们演示的数据表是不是也是用了InnoDB,可以看到确实是使用InnoDB

那我们把该表的引擎修改成MyISAM会怎么样呢?试试,在这里我们只修改数据表的数据引擎:

mysql> ALTER TABLE user ENGINE=MyISAM;Query OK, 2 rows affected (0.06 sec)Records: 2  Duplicates: 0  Warnings: 0</code-pre> 

然后再update,不出意料,还是会报错,看起来错误没有什么不同:

但是获取全部数据的时候,第一个数据更新成功了,第二个数据没有更新成功,说明事务没有生效。

<pre highlighted="true" class="mCustomScrollbar _mCS_7 mCS-autoHide mCS_no_scrollbar" id="pre-XiGnnx" style="padding: 10px !important; margin: 5px 0px; transition-duration: 0.2s; transition-property: color, opacity; overflow: visible; touch-action: auto; font-size: 14px !important; font-weight: 400; font-family: "Ubuntu Mono", monospace !important; white-space: pre; overflow-wrap: break-word; border-radius: 4px !important; word-break: break-all; counter-reset: itemcounter 0; color: rgb(0, 0, 0); background-color: rgb(246, 248, 250); position: relative;"><code-pre class="code-pre" id="pre-fc4yyy" style="padding: 0px 0px 0px 10px; margin: 0px; position: relative; display: block;">[{"id":1,"name":"李四","age":12},{"id":2,"name":"王五","age":11}]</code-pre> 

结论:必须设置为InnoDB引擎,事务才生效。

2. 方法不能是 private

事务必须是public方法,如果用在了private方法上,那么事务会自动失效,但是在IDEA中,只要我们写了就会报错:Methods annotated with '@Transactional' must be overrideable,意思是事务的注解加上的方法,必须是可以重写的,private方法是不可以重写的,所以报错了。

同样的final修饰的方法,如果加上了注解,也会报错,因为用final就是不想被重写:

Spring中主要是用放射获取Bean的注解信息,然后利用基于动态代理技术的AOP来封装了整个事务,理论上我想调用private方法也是没有问题的,在方法级别使用method.setAccessible(true);就可以,但是可能Spring团队觉得private方法就是开发人员意愿上不愿意公开的接口,没有必要破坏封装性,这样容易导致混乱。

Protected方法可不可以?不可以!

下面我们为了实现,魔改代码结构,因为接口不能用Portected,如果用了接口,就不可能用protected方法,会直接报错,而且必须在同一个包里面使用,我们把controllerservice放到同一个包下:

测试后发现事务不生效,结果依然是一个更新了,另外一个没有更新:

<pre highlighted="true" class="mCustomScrollbar _mCS_8 mCS-autoHide mCS_no_scrollbar" id="pre-WZ3yTF" style="padding: 10px !important; margin: 5px 0px; transition-duration: 0.2s; transition-property: color, opacity; overflow: visible; touch-action: auto; font-size: 14px !important; font-weight: 400; font-family: "Ubuntu Mono", monospace !important; white-space: pre; overflow-wrap: break-word; border-radius: 4px !important; word-break: break-all; counter-reset: itemcounter 0; color: rgb(0, 0, 0); background-color: rgb(246, 248, 250); position: relative;"><code-pre class="code-pre" id="pre-BDBBTe" style="padding: 0px 0px 0px 10px; margin: 0px; position: relative; display: block;">[{"id":1,"name":"李四","age":12},{"id":2,"name":"王五","age":11}]</code-pre> 

结论:必须使用在public方法上,不能用在private,finalstatic方法上,否则不会生效。

3. 异常必须是运行期的异常

Springboot管理异常的时候,只会对运行时的异常(RuntimeException 以及它的子类) 进行回滚,比如我们前面写的i=1/0;,就会产生运行时的异常。

从源码来看也可以看到,rollbackOn(ex)方法会判断异常是RuntimeException或者Error

<pre highlighted="true" class="mCustomScrollbar _mCS_9 mCS-autoHide mCS_no_scrollbar" id="pre-Zx55QT" style="padding: 10px !important; margin: 5px 0px; transition-duration: 0.2s; transition-property: color, opacity; overflow: visible; touch-action: auto; font-size: 14px !important; font-weight: 400; font-family: "Ubuntu Mono", monospace !important; white-space: pre; overflow-wrap: break-word; border-radius: 4px !important; word-break: break-all; counter-reset: itemcounter 0; color: rgb(0, 0, 0); background-color: rgb(246, 248, 250); position: relative;"> <code-pre class="code-pre" id="pre-DP8str" style="padding: 0px 0px 0px 10px; margin: 0px; position: relative; display: block;">public boolean rollbackOn(Throwable ex) {        return (ex instanceof RuntimeException || ex instanceof Error);    }</code-pre> 

异常主要分为以下类型:

所有的异常都是Throwable,而Error是错误信息,一般是程序发生了一些不可控的错误,比如没有这个文件,内存溢出,IO突然错误了。而Exception下,除了RuntimeException,其他的都是CheckException,也就是可以处理的异常,Java程序在编写的时候就必须处理这个异常,否则编译是通不过去的。

由下面的图我们可以看出,CheckedException,我列举了几个常见的IOException IO异常,NoSuchMethodException没有找到这个方法,ClassNotFoundException 没找到这个类,而RunTimeException有常见的几种:

数组越界异常:IndexOutOfBoundsException类型转换异常:ClassCastException空指针异常:NullPointerException

事务默认回滚的是:运行时异常,也就是RunTimeException,如果抛出其他的异常是无法回滚的,比如下面的代码,事务就会失效:

<pre highlighted="true" class="mCustomScrollbar _mCS_10 mCS-autoHide mCS_no_scrollbar" id="pre-2mPpBf" style="padding: 10px !important; margin: 5px 0px; transition-duration: 0.2s; transition-property: color, opacity; overflow: visible; touch-action: auto; font-size: 14px !important; font-weight: 400; font-family: "Ubuntu Mono", monospace !important; white-space: pre; overflow-wrap: break-word; border-radius: 4px !important; word-break: break-all; counter-reset: itemcounter 0; color: rgb(0, 0, 0); background-color: rgb(246, 248, 250); position: relative;"> <code-pre class="code-pre" id="pre-X7Ame6" style="padding: 0px 0px 0px 10px; margin: 0px; position: relative; display: block;">@Transactional     public void updateUserAge() throws Exception{        userMapper.updateUserAge(1);        try{            int i = 1/0;        }catch (Exception ex){            throw new IOException("IO异常");        }        userMapper.updateUserAge(2);    }</code-pre> 
4. 配置不对导致方法上需要使用@Transactional才能开启事务多个数据源配置或者多个事务管理器的时候,注意如果操作数据库A,不能使用B的事务,虽然这个问题很幼稚,但是有时候用错难查找问题。如果在Spring中,需要配置@EnableTransactionManagement来开启事务,等同于配置xml文件*<tx:annotation-driven/>*,但是在Springboot中已经不需要了,在springbootSpringBootApplication注解包含了@EnableAutoConfiguration注解,会自动注入。

@EnableAutoConfiguration自动注入了哪些东西呢?在jetbrains://idea/navigate/reference?project=springDocker&path=~/.m2/repository/org/springframework/boot/spring-boot-autoconfigure/2.5.6/spring-boot-autoconfigure-2.5.6.jar!/META-INF/spring.factories下有自动注入的配置:

<pre highlighted="true" class="mCustomScrollbar _mCS_11 mCS-autoHide mCS_no_scrollbar" id="pre-asjpBE" style="padding: 10px !important; margin: 5px 0px; transition-duration: 0.2s; transition-property: color, opacity; overflow: visible; touch-action: auto; font-size: 14px !important; font-weight: 400; font-family: "Ubuntu Mono", monospace !important; white-space: pre; overflow-wrap: break-word; border-radius: 4px !important; word-break: break-all; counter-reset: itemcounter 0; color: rgb(0, 0, 0); background-color: rgb(246, 248, 250); position: relative;"><code-pre class="code-pre" id="pre-5b8ZFf" style="padding: 0px 0px 0px 10px; margin: 0px; position: relative; display: block;"># Auto Configureorg.springframework.boot.autoconfigure.EnableAutoConfiguration=\org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\...org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration,\...</code-pre> 

里面配置了一个TransactionAutoConfiguration,这是事务自动配置类:

<pre highlighted="true" class="mCustomScrollbar _mCS_12 mCS-autoHide mCS_no_scrollbar" id="pre-MDyDxY" style="padding: 10px !important; margin: 5px 0px; transition-duration: 0.2s; transition-property: color, opacity; overflow: visible; touch-action: auto; font-size: 14px !important; font-weight: 400; font-family: "Ubuntu Mono", monospace !important; white-space: pre; overflow-wrap: break-word; border-radius: 4px !important; word-break: break-all; counter-reset: itemcounter 0; color: rgb(0, 0, 0); background-color: rgb(246, 248, 250); position: relative;"><code-pre class="code-pre" id="pre-WMDTnb" style="padding: 0px 0px 0px 10px; margin: 0px; position: relative; display: block;">@Configuration(proxyBeanMethods = false)@ConditionalOnClass(PlatformTransactionManager.class)@AutoConfigureAfter({ JtaAutoConfiguration.class, HibernateJpaAutoConfiguration.class,        DataSourceTransactionManagerAutoConfiguration.class, Neo4jDataAutoConfiguration.class })@EnableConfigurationProperties(TransactionProperties.class)public class TransactionAutoConfiguration {  ...    @Configuration(proxyBeanMethods = false)    @ConditionalOnBean(TransactionManager.class)    @ConditionalOnMissingBean(AbstractTransactionManagementConfiguration.class)    public static class EnableTransactionManagementConfiguration {        @Configuration(proxyBeanMethods = false)        @EnableTransactionManagement(proxyTargetClass = false)   // 这里开启了事务        @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false")        public static class JdkDynamicAutoProxyConfiguration {        }    ...    }}

值得注意的是,@Transactional除了可以用于方法,还可以用于类,表示这个类所有的public方法都会配置事务。

5. 事务方法不能在同个类里面调用

想要进行事务管理的方法只能在其他类里面被调用,不能在当前类被调用,否则会失效,为了实现这个目的,如果同一个类有不少事务方法,还有其他方法,这个时候有必要抽取出一个事务类,这样分层会比较清晰,避免后继者写的时候在同一个类调用事务方法,造成混乱。

事务失效的例子:

比如我们将service事务方法改成:

<pre highlighted="true" class="mCustomScrollbar _mCS_13 mCS-autoHide mCS_no_scrollbar" id="pre-7CW73F" style="padding: 10px !important; margin: 5px 0px; transition-duration: 0.2s; transition-property: color, opacity; overflow: visible; touch-action: auto; font-size: 14px !important; font-weight: 400; font-family: "Ubuntu Mono", monospace !important; white-space: pre; overflow-wrap: break-word; border-radius: 4px !important; word-break: break-all; counter-reset: itemcounter 0; color: rgb(0, 0, 0); background-color: rgb(246, 248, 250); position: relative;"> <code-pre class="code-pre" id="pre-yWdRaR" style="padding: 0px 0px 0px 10px; margin: 0px; position: relative; display: block;">public void testTransaction(){        updateUserAge();    }    @Transactional     public void updateUserAge(){        userMapper.updateUserAge(1);        int i = 1/0;        userMapper.updateUserAge(2);    }</code-pre> 

controller里面调用的是没有事务注解的方法,再间接调用事务方法:

<pre highlighted="true" class="mCustomScrollbar _mCS_14 mCS-autoHide mCS_no_scrollbar" id="pre-Mpp7xW" style="padding: 10px !important; margin: 5px 0px; transition-duration: 0.2s; transition-property: color, opacity; overflow: visible; touch-action: auto; font-size: 14px !important; font-weight: 400; font-family: "Ubuntu Mono", monospace !important; white-space: pre; overflow-wrap: break-word; border-radius: 4px !important; word-break: break-all; counter-reset: itemcounter 0; color: rgb(0, 0, 0); background-color: rgb(246, 248, 250); position: relative;"> <code-pre class="code-pre" id="pre-MKJsKN" style="padding: 0px 0px 0px 10px; margin: 0px; position: relative; display: block;">@RequestMapping("/update")    @ResponseBody    public int update() throws Exception{        userService.testTransaction();        return 1;    }</code-pre> 

调用之后,发现事务失效,一个更新另外一个没有更新:

<pre highlighted="true" class="mCustomScrollbar _mCS_15 mCS-autoHide mCS_no_scrollbar" id="pre-HjxN2B" style="padding: 10px !important; margin: 5px 0px; transition-duration: 0.2s; transition-property: color, opacity; overflow: visible; touch-action: auto; font-size: 14px !important; font-weight: 400; font-family: "Ubuntu Mono", monospace !important; white-space: pre; overflow-wrap: break-word; border-radius: 4px !important; word-break: break-all; counter-reset: itemcounter 0; color: rgb(0, 0, 0); background-color: rgb(246, 248, 250); position: relative;"><code-pre class="code-pre" id="pre-Thr7fE" style="padding: 0px 0px 0px 10px; margin: 0px; position: relative; display: block;">[{"id":1,"name":"李四","age":12},{"id":2,"name":"王五","age":11}]</code-pre> 

为什么会这样呢?

Spring用切面对方法进行包装,只对外部调用方法进行拦截,内部方法没有进行拦截。

看源码:实际上我们调用事务方法的时候,会进入DynamicAdvisedInterceptorpublic Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy)()方法:

里面调用了AdvisedSupport.getInterceptorsAndDynamicInterceptionAdvice(),这里是获取调用调用链。而没有@Transactional注解的方法userService.testTransaction(),根本获取不到代理调用链,调用的还是原来的类的方法。

spring里面要想对一个方法进行代理,用的就是aop,肯定需要一个标识,标识哪一个方法或者类需要被代理,spring里面定义了@Transactional作为切点,我们定义这个标识,就会被代理。

代理的时机是什么时候呢?

Spring统一管理了我们的bean,代理的时机自然就是创建bean的过程,看看哪一个类带了这个标识,就生成代理对象。

SpringTransactionAnnotationParser这个类有一个方法是用来判断TransactionAttribute注解的:

<pre highlighted="true" class="mCustomScrollbar _mCS_16 mCS-autoHide mCS_no_scrollbar" id="pre-sYESid" style="padding: 10px !important; margin: 5px 0px; transition-duration: 0.2s; transition-property: color, opacity; overflow: visible; touch-action: auto; font-size: 14px !important; font-weight: 400; font-family: "Ubuntu Mono", monospace !important; white-space: pre; overflow-wrap: break-word; border-radius: 4px !important; word-break: break-all; counter-reset: itemcounter 0; color: rgb(0, 0, 0); background-color: rgb(246, 248, 250); position: relative;"> <code-pre class="code-pre" id="pre-tAx3G4" style="padding: 0px 0px 0px 10px; margin: 0px; position: relative; display: block;">@Override    @Nullable    public TransactionAttribute parseTransactionAnnotation(AnnotatedElement element) {        AnnotationAttributes attributes = AnnotatedElementUtils.findMergedAnnotationAttributes(                element, Transactional.class, false, false);        if (attributes != null) {            return parseTransactionAnnotation(attributes);        }        else {            return null;        }  }
6.多线程下事务失效

假设我们在多线程里面像以下方式使用事务,那么事务是不能正常回滚的:

<pre highlighted="true" class="mCustomScrollbar _mCS_17 mCS-autoHide mCS_no_scrollbar" id="pre-BZfc3H" style="padding: 10px !important; margin: 5px 0px; transition-duration: 0.2s; transition-property: color, opacity; overflow: visible; touch-action: auto; font-size: 14px !important; font-weight: 400; font-family: "Ubuntu Mono", monospace !important; white-space: pre; overflow-wrap: break-word; border-radius: 4px !important; word-break: break-all; counter-reset: itemcounter 0; color: rgb(0, 0, 0); background-color: rgb(246, 248, 250); position: relative;"> <code-pre class="code-pre" id="pre-iCTaSf" style="padding: 0px 0px 0px 10px; margin: 0px; position: relative; display: block;">@Transactional    public void updateUserAge() {        new Thread(                new Runnable() {                    @Override                    public void run() {                        userMapper.updateUserAge(1);                    }                }        ).start();        int i = 1 / 0;        userMapper.updateUserAge(2);    } 

因为不同的线程使用的是不同SqlSession,相当于另外一个连接,根本不会用到同一个事务:

<pre highlighted="true" class="mCustomScrollbar _mCS_18 mCS-autoHide" id="pre-4YJBXj" style="padding: 10px !important; margin: 5px 0px; transition-duration: 0.2s; transition-property: color, opacity; overflow: visible; touch-action: pinch-zoom; font-size: 14px !important; font-weight: 400; font-family: "Ubuntu Mono", monospace !important; white-space: pre; overflow-wrap: break-word; border-radius: 4px !important; word-break: break-all; counter-reset: itemcounter 0; color: rgb(0, 0, 0); background-color: rgb(246, 248, 250); position: relative;"><code-pre class="code-pre" id="pre-j6ZkwS" style="padding: 0px 0px 0px 10px; margin: 0px; position: relative; display: block;">2021-11-28 14:06:59.852 DEBUG 52764 --- [       Thread-2] org.mybatis.spring.SqlSessionUtils       : Creating a new SqlSession2021-11-28 14:06:59.930 DEBUG 52764 --- [       Thread-2] c.a.s.mapper.UserMapper.updateUserAge    : <==    Updates: 12021-11-28 14:06:59.931 DEBUG 52764 --- [       Thread-2] org.mybatis.spring.SqlSessionUtils       : Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2e956409]</code-pre> 
7. 注意合理使用事务嵌套

首先事务是有传播机制的:

REQUIRED(默认):支持使用当前事务,如果当前事务不存在,创建一个新事务,如果有直接使用当前的事务。SUPPORTS:支持使用当前事务,如果当前事务不存在,就不会使用事务。MANDATORY:支持使用当前事务,如果当前事务不存在,则抛出Exception,也就是必须当前处于事务里面。REQUIRES_NEW:创建新事务,如果当前事务存在,把当前事务挂起。NOT_SUPPORTED:没有事务执行,如果当前事务存在,把当前事务挂起。NEVER:没有事务执行,如果当前有事务则抛出ExceptionNESTED:嵌套事务,如果当前事务存在,那么在嵌套的事务中执行。如果当前事务不存在,则表现跟`REQUIRED

查不多。

默认的是REQUIRED,也就是事务里面调用另外的事务,实际上不会重新创建事务,而是会重用当前的事务。那如果我们这样来写嵌套事务:

<pre highlighted="true" class="mCustomScrollbar _mCS_19 mCS-autoHide mCS_no_scrollbar" id="pre-wmfx5P" style="padding: 10px !important; margin: 5px 0px; transition-duration: 0.2s; transition-property: color, opacity; overflow: visible; touch-action: auto; font-size: 14px !important; font-weight: 400; font-family: "Ubuntu Mono", monospace !important; white-space: pre; overflow-wrap: break-word; border-radius: 4px !important; word-break: break-all; counter-reset: itemcounter 0; color: rgb(0, 0, 0); background-color: rgb(246, 248, 250); position: relative;"><code-pre class="code-pre" id="pre-453bAr" style="padding: 0px 0px 0px 10px; margin: 0px; position: relative; display: block;">@Service("userService")public class UserServiceImpl {    @Autowired    UserServiceImpl2 userServiceImpl2;    @Resource    UserMapper userMapper;    @Transactional    public void updateUserAge() {        try {            userMapper.updateUserAge(1);            userServiceImpl2.updateUserAge();        }catch (Exception ex){            ex.printStackTrace();        }    }}

调用的另外一个事务:

<pre highlighted="true" class="mCustomScrollbar _mCS_20 mCS-autoHide mCS_no_scrollbar" id="pre-WxpZ2X" style="padding: 10px !important; margin: 5px 0px; transition-duration: 0.2s; transition-property: color, opacity; overflow: visible; touch-action: auto; font-size: 14px !important; font-weight: 400; font-family: "Ubuntu Mono", monospace !important; white-space: pre; overflow-wrap: break-word; border-radius: 4px !important; word-break: break-all; counter-reset: itemcounter 0; color: rgb(0, 0, 0); background-color: rgb(246, 248, 250); position: relative;"><code-pre class="code-pre" id="pre-8PGNb2" style="padding: 0px 0px 0px 10px; margin: 0px; position: relative; display: block;">@Service("userService2")public class UserServiceImpl2 {    @Resource    UserMapper userMapper;    @Transactional    public void updateUserAge() {        userMapper.updateUserAge(2);        int i = 1 / 0;    }}

会抛出以下错误:

<pre highlighted="true" class="mCustomScrollbar _mCS_21 mCS-autoHide" id="pre-5r2TF6" style="padding: 10px !important; margin: 5px 0px; transition-duration: 0.2s; transition-property: color, opacity; overflow: visible; touch-action: pinch-zoom; font-size: 14px !important; font-weight: 400; font-family: "Ubuntu Mono", monospace !important; white-space: pre; overflow-wrap: break-word; border-radius: 4px !important; word-break: break-all; counter-reset: itemcounter 0; color: rgb(0, 0, 0); background-color: rgb(246, 248, 250); position: relative;"><code-pre class="code-pre" id="pre-ERrAhM" style="padding: 0px 0px 0px 10px; margin: 0px; position: relative; display: block;">org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only

我们但是实际事务是正常回滚掉了,结果是对的,之所以出现这个问题,是因为里面到方法抛出了异常,用的是同一个事务,说明事务必须被回滚掉的,但是外层被catch住了,本来就是同一个事务,一个说回滚,一个catch住不让spring感知到Exception,那不是自相矛盾么?所以spring报错说:这个事务被标识了必须回滚掉,最终还是回滚掉了。

怎么处理呢?

外层主动抛出错误,throw new RuntimeException()使用TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();主动标识回滚

<pre highlighted="true" class="mCustomScrollbar _mCS_22 mCS-autoHide mCS_no_scrollbar" id="pre-2xdy6x" style="padding: 10px !important; margin: 5px 0px; transition-duration: 0.2s; transition-property: color, opacity; overflow: visible; touch-action: auto; font-size: 14px !important; font-weight: 400; font-family: "Ubuntu Mono", monospace !important; white-space: pre; overflow-wrap: break-word; border-radius: 4px !important; word-break: break-all; counter-reset: itemcounter 0; color: rgb(0, 0, 0); background-color: rgb(246, 248, 250); position: relative;"> <code-pre class="code-pre" id="pre-hdbWeM" style="padding: 0px 0px 0px 10px; margin: 0px; position: relative; display: block;">@Transactional    public void updateUserAge() {        try {            userMapper.updateUserAge(1);            userServiceImpl2.updateUserAge();        }catch (Exception ex){            ex.printStackTrace();            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();        }    }
8. 依赖外部网络请求回滚需要考虑

有些时候,我们不仅操作自己的数据库,还需要同时考虑外部的请求,比如同步数据,同步失败,需要回滚掉自己的状态,在这种场景下,必须考虑网络请求是否会出错,出错如何处理,错误码是哪一个的时候才成功。

如果网络超时了,实际上成功了,但是我们判定为没有成功,回滚掉了,可能会导致数据不一致。这种需要被调用方支持重试,重试的时候,需要支持幂等,多次调用保存状态的一致,虽然整个主流程很简单,里面的细节还是比较多的。

总结

事务被Spring包裹了复杂性,很多东西可能源码很深,我们用的时候注意模拟测试一下调用是不是能正常回滚,不能理所当然,人是会出错的,而很多时候黑盒测试根本测试这种异常数据,如果没有正常回滚,后面需要手动处理,考虑到系统之间同步的问题,会造成很多不必要的麻烦,手动改数据库这流程就必须走。

原文:

标签: #mysql事务超时自动回滚