spring的事务机制
https://blog.csdn.net/minghao0508/article/details/124374637
1、事务的基本概念:
在数据库系统中,一个事务是指:由一系列数据库操作组成的一个完整的逻辑过程;例如银行转帐,从原账户扣除金额,以及向目标账户添加金额,这两个数据库操作的总和,构成一个完整的逻辑过程,不可拆分;这个过程被称为一个事务,具有ACID特性;
2、事务的传播行为
https://zhuanlan.zhihu.com/p/148504094
https://cuizb.top/myblog/article/detail/1667390158#%E4%BA%8B%E5%8A%A1%E7%9A%84%E4%BC%A0%E6%92%AD%E8%A1%8C%E4%B8%BA%EF%BC%88propagation%20behavior%EF%BC%89
指的就是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行。
例如:methodA事务方法调用methodB事务方法时,methodB是继续在调用者methodA的事务中运行呢,还是为自己开启一个新事务运行,这就是由methodB的事务传播行为决定的。
Spring默认事务传播行为是:propagation_required
1. TransactionDefinition.PROPAGATION_REQUIRED:
"如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。这是默认值。"
2. TransactionDefinition.PROPAGATION_REQUIRES_NEW:
"创建一个新的事务,如果当前存在事务,则把当前事务挂起。"
3. TransactionDefinition.PROPAGATION_SUPPORTS:
"如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。"
4. TransactionDefinition.PROPAGATION_NOT_SUPPORTED:
"以非事务方式运行,如果当前存在事务,则把当前事务挂起。"
5. TransactionDefinition.PROPAGATION_NEVER:
"以非事务方式运行,如果当前存在事务,则抛出异常。"
6. TransactionDefinition.PROPAGATION_MANDATORY:
"如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。"
7. TransactionDefinition.PROPAGATION_NESTED:
"如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;"
"如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED。"
3、事务的底层实现原理
Spring AOP将通用的功能横向抽取出来作为切面,避免非业务代码侵入到业务代码中;通过@Transactional注解就能让Spring为我们管理事务,免去了重复的事务管理逻辑,减少对业务代码的侵入,让开发人员能够专注于业务层面开发;
结合上面的图,我们关注两点:
(1)判断生成代理对象:通过@Transactional注解来标记方法(定义切点),在Bean初始化过程中判断是否要对当前Bean创建代理对象,并且拿到@Transactional注解的属性;
(2)定义代理对象的回调逻辑,即执行代理逻辑:在执行目标方法前打开事务,执行过程中捕获异常执行回滚逻辑,在执行完目标方法后提交事务;
源码分析请参考我的文章《Spring事务注解@Transactional的源码分析》,将源码的流画张图总结下:
4、事务失效场景
https://blog.csdn.net/Weixiaohuai/article/details/120804192
(1)@Transactional注解未打在public方法上
以下来自 Spring 官方文档:
When using proxies, you should apply the @Transactional annotation only to methods with public visibility. If you do annotate protected, private or package-visible methods with the @Transactional annotation, no error is raised, but the annotated method does not exhibit the configured transactional settings. Consider the use of AspectJ (see below) if you need to annotate non-public methods.
大概意思就是 @Transactional
只能用于 public 的方法上,否则事务不会失效,如果要用在非 public 方法上,可以开启 AspectJ
代理模式。
(2)目标方法用final、static修饰
Spring 使用 AOP(面向切面编程)来实现事务管理。当一个对象被 Spring 托管并使用 @Transactional
注解标记时,Spring 会使用动态代理或者CGLIB(如果被代理的类没有实现接口)创建一个代理对象来处理事务。
-
JDK 动态代理: 如果你的类有接口并且 final 方法所在的类通过 JDK 动态代理,则 final 方法可能不会被代理,因为 JDK 动态代理无法覆盖 final 方法。
-
CGLIB 代理: 如果使用 CGLIB 代理,final 方法应该能够被代理,但可能存在其他问题或者配置错误。
JDK动态代理的限制: JDK动态代理是基于接口的,它无法代理静态方法。它会生成目标类的一个实现接口的代理类,而静态方法是无法实现接口的。
- CGLIB代理的不支持: CGLIB代理可以代理类而不是接口,但它也不能代理静态方法。CGLIB是通过生成目标类的子类来实现代理,而静态方法是不能被子类覆盖
(3)同一个类中的方法直接内部调用
方法自调用问题:非事务方法insert()中调用的自身类的事务方法insertUser()。
下面我们来看一个事务失效的例子:
@Override public void insert(User user) { //如果insertUser方法内发生了异常,事务没有生效,数据还是插入进去了. this.insertUser(user); } @Override @Transactional public void insertUser(User user) { userMapper.insertUser(user); //模拟发生异常 int i = 1 / 0; }
启动程序,执行insert()方法,程序发生异常:
查看数据库:
我们发现数据库的数据插入成功,事务失效了,并没有回滚。
原因分析:
由于@Transactional的实现原理是AOP,而AOP的实现原理是动态代理,而自己调用自己的过程,并不存在代理对象的调用,这样就不会产生AOP去为我们设置@Transactional配置的参数,这样就出现了自调用注解失效的问题。
解决方式一:把insert(User user)和insertUser(User user)拆开来,分别放到不同的类里。
解决方式二:自己注入自己,用注入的实例调用。
如下:
@Service public class UserServiceImpl implements UserService { @Autowired private UserMapper userMapper; @Autowired private UserService userService; @Override public void insert(User user) { //注意,这里不使用this调用,而是@Autowired自己(UserService)注入自己,然后调用事务方法insertUser userService.insertUser(user); } @Override @Transactional public void insertUser(User user) { userMapper.insertUser(user); //模拟发生异常 int i = 1 / 0; } }
启动程序,调用insert(User user)方法,程序发生异常,查看数据库:
我们发现,此时数据库数据并没有插入新的数据,事务成功回滚了,事务生效。
解决方式三:获取代理类,利用代理类调用自己类的方法。
如下:
@Service public class UserServiceImpl implements UserService { @Autowired private UserMapper userMapper; @Override public void insert(User user) { //获取代理类,利用代理类调用自己类的方法 ((UserService) AopContext.currentProxy()).insertUser(user); } @Override @Transactional public void insertUser(User user) { userMapper.insertUser(user); //模拟发生异常 int i = 1 / 0; } }
再次程序,我们发现程序抛出异常,查看数据库数据:
我们发现,此时数据库数据并没有插入新的数据,事务成功回滚了,事务生效。
解决方式四:去掉insertUser(User user)上面的@Transactional注解,把它加到insert(User user)方法上, 这样能使用到 insert(User user)的事务。
@Override @Transactional public void insert(User user) { try { this.insertUser(user); } catch (Exception e) { e.printStackTrace(); //注意这里,需要抛出Spring事务支持的异常类型,否则事务还是会失效的 throw new RuntimeException(); } } @Override public void insertUser(User user) { userMapper.insertUser(user); //模拟发生异常 int i = 1 / 0; }
再次程序,我们发现程序抛出异常,查看数据库数据:
我们发现,此时数据库数据并没有插入新的数据,事务成功回滚了,事务生效。
(4)事务方法所在的类未被Spring管理
如下面例子所示:
// @Service
public class OrderServiceImpl implements OrderService {
@Transactional
public void updateOrder(Order order) {
// update order
}
}
如果此时把 @Service
注解注释掉,这个类就不会被加载成一个 Bean,那这个类就不会被 Spring 管理了,事务自然就失效了。
(5)多线程调用
@Service public class UserService { @Autowired private UserMapper userMapper; @Autowired private RoleService roleService; @Transactional public void add(UserModel userModel) throws Exception { userMapper.insertUser(userModel); new Thread(() -> { roleService.doOtherThing(); }).start(); } } @Service public class RoleService { @Transactional public void doOtherThing() { System.out.println("保存role表数据"); } }
从上面的例子中,我们可以看到事务方法add中,调用了事务方法doOtherThing,但是事务方法doOtherThing是在另外一个线程中调用的。这样会导致两个方法不在同一个线程中,获取到的数据库连接不一样,从而是两个不同的事务。如果想doOtherThing方法中抛了异常,add方法也回滚是不可能的。
我们说的同一个事务,其实是指同一个数据库连接,只有拥有同一个数据库连接才能同时提交和回滚。如果在不同的线程,拿到的数据库连接肯定是不一样的,所以它们是不同的事务。
(6)存储引擎不支持事务
如果使用mysql且存储引擎是MyISAM,则事务会不起作用,原因是MyISAM不支持事务。
解决方式:数据表可以改成InnoDB存储引擎,支持事务。
(7)传播类型不支持事务,导致事务失效
@Service public class OrderServiceImpl implements OrderService { @Transactional public void update(Order order) { updateOrder(order); } //以非事务方式运行 @Transactional(propagation = Propagation.NOT_SUPPORTED) public void updateOrder(Order order) { // update order } }