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

一箫一剑平生意,负尽狂名十五年。这篇文章主要讲述完蛋,我的事务怎么不生效?相关的知识,希望能为你提供帮助。
前言事务大家平时应该都有写,之前写事务的时候遇到一点坑,居然不生效,后来排查了一下,复习了一下各种事务失效的场景,想着不如来一个总结,这样下次排查问题,就能有恃无恐了。那么先来复习一下事务相关知识,事务是指操作的最小工作单位,作为一个单独且不可切割的单元操作,要么全部成功,要么全部失败。事务有四大特性(??ACID??):

  • 原子性(??Atomicity??):事务包含的操作,要么全部成功,要么全部失败回滚,不会存在一半成功一半失败的中间状态。比如??A??和??B??一开始都有??500??元,??A??给??B??转账??100??,那么??A??的钱少了??100??,??B??的钱就必须多了??100??,不能??A??少了钱,??B??也没收到钱,那这个钱就不翼而飞了,不符合原子性了。
  • 一致性(??Consistency??):一致性是指事务执行之前和之后,保持整体状态的一致,比如??A??和??B??一开始都有??500??元,加起来是??1000??元,这个是之前的状态,??A??给??B??转账??100??,那么最后??A??是??400??,??B??是??600??,两者加起来还是??1000??,这个整体状态需要保证。
  • 隔离性(??Isolation??):前面两个特性都是针对同一个事务的,而隔离性指的是不同的事务,当多个事务同时在操作同一个数据的时候,需要隔离不同事务之间的影响,并发执行的事务之间不能相互干扰。
  • 持久性(??Durability??):指事务如果一旦被提交了,那么对数据库的修改就是永久性的,就算是数据库发生故障了,已经发生的修改也必然存在。
事务的几个特性并不是数据库事务专属的,广义上的事务是一种工作机制,是并发控制的基本单位,保证操作的结果,还会包括分布式事务之类的,但是一般我们谈论事务,不特指的话,说的就是与数据库相关的,因为我们平时说的事务基本都基于数据库来完成。
事务不仅是适用于数据库。我们可以将此概念扩展到其他组件,类似队列服务或外部系统状态。因此,“一系列数据操作语句必须完全完成或完全失败,以一致的状态离开系统”
测试环境
前面我们已经部署过了一些demo项目,以及用docker快速搭建环境,本文基于的也是之前的环境:
  • JDK 1.8
  • Maven 3.6
  • Docker
  • mysql
事务正常回滚的样例
正常的事务样例,包含两个接口,一个是获取所有的用户中的数据,另外一个更新的,是??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" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
< 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>

先获取??http://localhost:8081/getUserList??所有的用户看看:

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

控制台也出现了异常,意思是除以0,异常:
java.lang.ArithmeticException: / by zero
at com.aphysia.springdocker.service.impl.UserServiceImpl.updateUserAge(UserServiceImpl.java:35) < sub> [classes/:na]
at com.aphysia.springdocker.service.impl.UserServiceImpl$$FastClassBySpringCGLIB$$c8cc4526.invoke(< generated> ) < /sub> [classes/:na]
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218) < sub> [spring-core-5.3.12.jar:5.3.12]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:783) < /sub> [spring-aop-5.3.12.jar:5.3.12]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) < sub> [spring-aop-5.3.12.jar:5.3.12]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:753) < /sub> [spring-aop-5.3.12.jar:5.3.12]
at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:123) < sub> [spring-tx-5.3.12.jar:5.3.12]
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:388) < /sub> [spring-tx-5.3.12.jar:5.3.12]
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119) < sub> [spring-tx-5.3.12.jar:5.3.12]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) < /sub> [spring-aop-5.3.12.jar:5.3.12]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:753) < sub> [spring-aop-5.3.12.jar:5.3.12]
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:698) < /sub> [spring-aop-5.3.12.jar:5.3.12]
at com.aphysia.springdocker.service.impl.UserServiceImpl$$EnhancerBySpringCGLIB$$25070cf0.updateUserAge(< generated> ) ~[classes/:na]

然后我们再次请求??http://localhost:8081/getUserList??,看到数据两个都是??11??说明数据都没有发生变化,第一个操作完之后,异常,回滚成功了:
["id":1,"name":"李四","age":11,"id":2,"name":"王五","age":11]

那什么时候事务不正常回滚呢?且听我细细道来:
实验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 |
+------------------------+--------+

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

那我们把该表的引擎修改成??MyISAM??会怎么样呢?试试,在这里我们只修改数据表的数据引擎:
mysql> ALTER TABLE user ENGINE=MyISAM;
Query OK, 2 rows affected (0.06 sec)
Records: 2Duplicates: 0Warnings: 0

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

但是获取全部数据的时候,第一个数据更新成功了,第二个数据没有更新成功,说明事务没有生效。
["id":1,"name":"李四","age":12,"id":2,"name":"王五","age":11]

结论:必须设置为??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??方法,会直接报错,而且必须在同一个包里面使用,我们把??controller??和??service??放到同一个包下:

测试后发现事务不生效,结果依然是一个更新了,另外一个没有更新:
["id":1,"name":"李四","age":12,"id":2,"name":"王五","age":11]

结论:必须使用在??public??方法上,不能用在??private??,??final??,??static??方法上,否则不会生效。
3. 异常必须是运行期的异常
??Springboot??管理异常的时候,只会对运行时的异常(??RuntimeException?? 以及它的子类) 进行回滚,比如我们前面写的??i=1/0; ??,就会产生运行时的异常。从源码来看也可以看到,?


【完蛋,我的事务怎么不生效()】?rollbackOn(ex)??方法会判断异常是?

    推荐阅读