目录
Spring中事务的实现
MySQL中的事务使用
Spring 编程式事务
TransactionTemplate 编程式事务
TransactionManager编程式事务
Spring声明式事务
@Transactional 参数说明
事务因为程序异常捕获不会自动回滚的解决方案
@Transactional 原理
Spring 事务隔离级别
Spring事务失效的场景
非public修饰的方法
为什么非public修饰的方法使用@Transactional会失效?
timeout超时
代码中有try/catch
调用类内部@Transactional方法
数据库不支持事务
Spring 事务传播机制
事务隔离级别和传播机制有什么区别?
① REQUIRES_NEW: 新建事务执行,如果当前存在事务,把当前事务挂起。
② REQUIRED (默认值):如果当前方法没有事务,新建一个事务,如果已经存在一个事务中,则加入到这个事务中。
③ NESTED(嵌套事务):如果当前存在事务,则在嵌套事务内执行。如果不存在,则执行与PROPAGATION_REQUIRED 类似的操作。
加入事务和嵌套事务有什么区别?
嵌套事务的实现原理:
Spring中事务的实现
Spring中的事务操作分为两类:
- 编程式事务 (手动写代码操作事务)
- 声明式事务(利用注解自动开启和提交事务)
MySQL中的事务使用
事务在MySQL有3个重要的操作:开启事务、提交事务、回滚事务,它们对应的操作命令如下:
-- 开启事务
start transaction;
-- 业务执行
-- 提交事务
commit;
-- 回滚事务
rollback;
Spring 编程式事务
Spring 手动操作事务和上面MySQL操作事务类似,他也是有3个重要操作步骤:
- 开启事务(获取事务)
- 提交事务
- 回滚事务
编程式事务有两种实现方式
- 使用 TransactionTemplate 对象实现编程式事务
- 使用更加底层的 TransactionManager 对象实现编程式事务
TransactionTemplate 编程式事务
要使用 TransactionTemplate 对象需要先将 TransactionTemplate 注入到当前类中,然后再使用它提供的 execute 方法执行事务并返回相应的执行结果,如果程序在执行途中出现了异常,那么就可以使用代码手动回滚事务,具体实现代码如下:
TransactionManager编程式事务
TransactionManager 实现编程式事务相对麻烦一点,它需要使用两个对象TransactionManager 的子类,加上 TransactionDefinition 事务定义对象,再通过调用TransactionManager的 getTransaction 获取并开启事务,然后调用 TransactionManager 提供的 commit 方法提交事务,或使用它的另一个方法 rollback 回滚事务,它的具体实现代码如下:
SpringBoot内置了两个对象:
- DataSourceTransactionManager: 事务的管理器, 是用来获取事务(开启事务)、提交或回滚事务的。
- TransactionDefinition :事务的属性,在获取事务的时候需要将TransactionDefinition传递进去从而获得一个事务 TransactionStatus。
实现代码如下:
package com.example.demo.controller;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController // 这是@Controller + @ResponseBody 的结果
//目的是返回非页面数据
@RequestMapping("/user")
public class UserController {
//编程式事务
@Autowired
private DataSourceTransactionManager dataSourceTransactionManager;
@Autowired
private TransactionDefinition transactionDefinition;
@Autowired
private UserService userService;
@RequestMapping("/del")
public int del(Integer id) {
if (id == null || id <= 0) return 0;
TransactionStatus transactionStatus = null;
int result = 0;
try {
// 1. 开启事务
transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition);
// 业务操作 删除用户
result = userService.del(id);
System.out.println("删除:" + result);
dataSourceTransactionManager.commit(transactionStatus); // 提交事务
} catch (Exception e) {
if (transactionStatus != null) {
dataSourceTransactionManager.rollback(transactionStatus); // 回滚事务
}
}
return result;
}
}
Spring声明式事务
声明式事务:只需要事务的⽅法上添加@Transactional 注解就可以实现了
⽆需⼿动开启事务和提交事务,添加该注解后实现的效果如下:
- 进⼊⽅法时自动开启事务。
- 方法执行完会自动提交事务。
- 如果中途发⽣了没有处理的异常会自动回滚事务。
@RestController
public class UserController2 {
@Autowired
private UserService userService;
@RequestMapping("/del")
@Transactional
public int del(Integer id) {
if (id == null || id <= 0) return 0;
int result = userService.del(id);
System.out.println("删除: " + result);
// int num = 10 / 0;
return result;
}
}
如果上面的异常触发,就会导致事务回滚,如果没有,事务则正常提交。
需要注意的一点是:这里的@Transactional 与单元测试中的@Transactional不一样。
单元测试中的@Transactional 是: 无论方法是执行完方法后,一定回滚事务。
@Transactional的作用
@Transactional 可以⽤来修饰方法或类:
- 修饰⽅法时: 只有修饰public⽅法时才⽣效(修饰其他⽅法时不会报错,也不生效)[推荐]
- 修饰类时: 对 @Transactional 修饰的类中所有的public方法都生效.
@Transactional 参数说明
需要注意的是:
@Transactional 在异常被捕获的情况下,不会进行事务自动回滚:
package com.example.demo.controller;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/user2")
public class UserController2 {
@Autowired
private UserService userService;
@RequestMapping("/del")
@Transactional()
public int del(Integer id) {
if (id == null || id <= 0) return 0;
int result = userService.del(id);
System.out.println("删除: " + result);
try {
int num = 10 / 0;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
}
观察代码运行结果,可以发现:由于异常被程序捕获(try-catch),事务不会进行回滚,数据被删除:
其实这是因为 @Transactional 在底层实现的时候是通过代理类来完成的,代理类会通过反射拿到目标方法,如果目标方法出现异常会进行回滚操作,否则就提交当前事务。
因此,如果我们在程序中显示的捕获异常,那么 @Transactional 里面的代理类就无法捕获到异常,于是就提交了事务。
事务因为程序异常捕获不会自动回滚的解决方案
方案①:将异常重新抛出
对于捕获的异常,事务是会自动回滚的,因此解决方案就是将异常重新抛出:
package com.example.demo.controller;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/user2")
public class UserController2 {
@Autowired
private UserService userService;
@RequestMapping("/del")
@Transactional()
public int del(Integer id) {
if (id == null || id <= 0) return 0;
int result = userService.del(id);
System.out.println("删除: " + result);
try {
int num = 10 / 0;
} catch (Exception e) {
throw e;
}
return result;
}
}
运行结果:
方案②:手动回滚事务
⼿动回滚事务,在⽅法中使⽤ TransactionAspectSupport.currentTransactionStatus() 可
以得到当前的事务,然后设置回滚⽅法 setRollbackOnly 就可以实现回滚了:
@Transactional 原理
@Transactional 是基于 AOP 实现的,AOP ⼜是使⽤动态代理实现的。如果⽬标对象实现了接⼝,默认情况下会采⽤ JDK 的动态代理,如果⽬标对象没有实现了接⼝,会使⽤ CGLIB 动态代理。
@Transactional 在开始执⾏业务之前,通过代理先开启事务,在执⾏成功之后再提交事务。如果中途遇到的异常,则回滚事务:
实现细节:
Spring 事务隔离级别
Spring 中事务隔离级别包含以下 5 种:
- Isolation.DEFAULT:以连接的数据库的事务隔离级别为主。
- Isolation.READ_UNCOMMITTED:读未提交,可以读取到未提交的事务,存在脏读。
- Isolation.READ_COMMITTED:读已提交,只能读取到已经提交的事务,解决了脏读,存在不可重复读。
- Isolation.REPEATABLE_READ:可重复读,解决了不可重复读,但存在幻读(MySQL默认级别)。
- Isolation.SERIALIZABLE:串⾏化,可以解决所有并发问题,但性能太低。
相比于 MySQL 的事务隔离级别,Spring 的事务隔离级别只是多了⼀个 Isolation.DEFAULT(以数据库的全局事务隔离级别为主)。
Spring 中事务隔离级别只需要设置 @Transactional ⾥的 isolation 属性即可,具体实现代码如下:
Spring事务失效的场景
在开始之前,我们先要明确一个定义,什么叫做“失效”?
本文中的“失效”指的是“失去(它的)功效”,也就是当 @Transactional 不符合我们预期的结果时,我们就可以说 @Transactional 失效了。
那 @Transactional 失效的场景有哪些呢?接下来我们一一来看。
非public修饰的方法
当 @Transactional 修饰的方法为非 public 时,事务就失效了,比如以下代码当遇到异常之后,不能自动实现回滚:
@Transactional
@RequestMapping("/save")
int save(UserInfo userInfo) {
// 非空效验
if (userInfo == null ||
!StringUtils.hasLength(userInfo.getUsername()) ||
!StringUtils.hasLength(userInfo.getPassword()))
return 0;
// 执行添加操作
int result = userService.save(userInfo);
System.out.println("add 受影响的行数:" + result);
int num = 10 / 0; // 此处设置一个异常
return result;
}
为什么非public修饰的方法使用@Transactional会失效?
答: 这分为两层原因: 浅层原因 和 深层次原因.
1. 浅层原因
浅层原因是 @Transactional 源码限制了必须是 public 才能执行后续的代理流程,它的部分实现源码如下:
protected TransactionAttribute computeTransactionAttribute(Method method, Class<?>
targetClass) {
// Don't allow no-public methods as required.
// 非 public 方法,设置为 nul1
if(allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}
// 后面代码省略....
}
2. 深层次原因
深层次的原因 Spring Boot 的动态代理只能代理公共方法,而不能代理私有方法或受保护的方法。
这是因为 Spring 的动态代理是基于 Java 的接口代理机制或者基于 CGLib 库来实现的,而这两种代理方式都只能代理公共方法。
- 接口代理: 当目标类实现了接口时,Spring 使用JDK 动态代理来生成代理对象。JDK 动态代理是通过生成实现目标类接口的匿名类,并将方法调用委托给目标类的实例来实现的。由于接口中的方法都是公共的,所以JDK 动态代理只能代理公共方法。
- CGLib 代理: 当目标类没有实现接门时,Spring 使用 CGLib 动态代理来生成代理对象CGLib 动态代理是通过生成目标类的子类,并将方法调用委托给子类的实例来实现的。然而Java 中的继承要求子类能够继承父类的方法,因此 CGLib 动态代理也只能代理目标类中的公共方法。
timeout超时
当在 @Transactional 上,设置了一个较小的超时时间时,如果方法本身的执行时间超过了设置的 timeout 超时时间,那么就会导致本来应该正常插入数据的方法执行失败,示例代码如下:
@Transactional(timeout = 3) // 超时时间为 3s
@RequestMapping("/save")
int save(UserInfo userInfo) throws InterruptedException {
// 非空效验
if (userInfo == null ||
!StringUtils.hasLength(userInfo.getUsername()) ||
!StringUtils.hasLength(userInfo.getPassword()))
return 0;
int result = userService.save(userInfo);
return result;
}
UserService 的 save 方法实现如下:
public int save(UserInfo userInfo) throws InterruptedException {
// 休眠 5s
TimeUnit.SECONDS.sleep(5);
int result = userMapper.add(userInfo);
return result;
}
代码中有try/catch
在前面 @Transactional 的执行流程中,我们提到:当方法中出现了异常之后,事务会自动回滚。然而,如果在程序中加了 try/catch 之后,@Transactional 就不会自动回滚事务了,示例代码如下:
@Transactional
@RequestMapping("/save")
public int save(UserInfo userInfo) throws InterruptedException {
// 非空效验
if (userInfo == null ||
!StringUtils.hasLength(userInfo.getUsername()) ||
!StringUtils.hasLength(userInfo.getPassword()))
return 0;
int result = userService.save(userInfo);
try {
int num = 10 / 0; // 此处设置一个异常
} catch (Exception e) {
}
return result;
}
调用类内部@Transactional方法
当调用类内部的 @Transactional 修饰的方法时,事务是不会生效的,示例代码如下:
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class MyService {
// 这个outerMethod方法的@Transactional无所谓有没有标注
@Transactional
public void outerMethod() {
// 调用内部方法
innerMethod();
}
@Transactional
public void innerMethod() {
// 这里的事务可能不会生效
// ...
}
}
说明:因为 @Transactional 是基于动态代理实现的,而当调用类内部的方法时,不是通过代理对象完成的,而是通过 this 对象实现的,这样就绕过了代理对象,从而事务就失效了。
数据库不支持事务
我们程序中的 @Transactional 只是给调用的数据库发送了:开始事务、提交事务、回滚事务的指令,但是如果数据库本身不支持事务,比如 MySQL 中设置了使用 MyISAM 引擎,那么它本身是不支持事务的,这种情况下,即使在程序中添加了 @Transactional 注解,那么依然不会有事务的行为,这就是巧妇也难为无米之炊吧。
Spring 事务传播机制
Spring 事务传播机制定义了多个包含了事务的⽅法: 相互调用时,事务是如何在这些⽅法间进⾏传递的。
比如有两个⽅法A,B都被 @Transactional 修饰,A⽅法调⽤B⽅法
A方法运行时,会开启⼀个事务.当A调⽤B时,B⽅法本⾝也有事务,此时B⽅法运⾏时,是加⼊A的事务,还是创建⼀个新的事务呢?这个就涉及到了事务的传播机制.
比如公司流程管理
执行任务之前,需要先写执行⽂档,任务执行结束,再写总结汇报。
此时A部门有⼀项⼯作,需要B部门的⽀援,此时B部门是直接使⽤A部门的⽂档,还是新建⼀个⽂档呢?
事务隔离级别和传播机制有什么区别?
事务的隔离级别是保证多个并发事务执行的可控性(稳定性),而事务传播机制是保证一个事务在多个调用方法间的可控性(稳定性)。
事务隔离级别描述的是多个事务同时执行时的某种行为;
而事务传播机制是描述包含了多个事务的方法在相互调用时事务的传播行为。
所以事务隔离级别描述的是纵向事务并发调用时的行为模式,而事务传播机制描述的是横向事务传递时的行为模式,如下图所示:
Spring 事务传播机制包含以下 7 种:
- Propagation.REQUIRED:默认的事务传播级别,它表示如果当前存在事务,则加⼊该事务;如果当前没有事务,则创建⼀个新的事务。
- Propagation.SUPPORTS:如果当前存在事务,则加⼊该事务;如果当前没有事务,则以⾮事务的⽅式继续运⾏。
- Propagation.MANDATORY:(mandatory:强制性)如果当前存在事务,则加⼊该事务;如果当前没有事务,则抛出异常。
- Propagation.REQUIRES_NEW:表示创建⼀个新的事务,如果当前存在事务,则把当前事务挂起。也就是说不管外部⽅法是否开启事务,Propagation.REQUIRES_NEW 修饰的内部⽅法会新开启⾃⼰的事务,且开启的事务相互独⽴,互不⼲扰。
- Propagation.NOT_SUPPORTED:以⾮事务⽅式运⾏,如果当前存在事务,则把当前事务挂起。
- Propagation.NEVER:以⾮事务⽅式运⾏,如果当前存在事务,则抛出异常。
- Propagation.NESTED:如果当前存在事务,则创建⼀个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于 PROPAGATION_REQUIRED。
以上 7 种传播⾏为,可以根据是否⽀持当前事务分为以下 3 类:
为了加深印象,我们这里就演示其中的三种传播机制:
调用链关系:Controller --> UserService -> LogService
Controller:
UserService:
LogService:
方便演示,这里更改传播机制时候是整个调用链一起修改。
① REQUIRES_NEW: 新建事务执行,如果当前存在事务,把当前事务挂起。
预期运行结果:用户添加成功,日志添加失败。
实际运行结果:没有添加任何记录。
分析:因为没有将程序异常进行处理,导致整条调用链上的事务感知到,从而全部回滚。
改进方法,将异常捕获,进行回滚:
运行结果:符合预期,日志出现回滚,用户加入操作没有回滚。
② REQUIRED (默认值)
:如果当前方法没有事务,新建一个事务,如果已经存在一个事务中,则加入到这个事务中。
预期运行结果:全部事务都进行回滚。
运行结果:全部事务进行回滚。
这里即使我们进行异常处理,还是出现报错的原因是因为:这里使用REQUIRED 是把这些事务看成一个整体,而事务外部觉得不应该回滚,因为没有出现异常,而事务内部进行回滚了,所以这里程序不知道什么情况,所以报500了。
但是如果是外部事务进行回滚,那么内部事务也会进行回滚。(这时候不会报异常)
③ NESTED(嵌套事务):如果当前存在事务,则在嵌套事务内执行。如果不存在,则执行与PROPAGATION_REQUIRED 类似的操作。
执行结果,日志添加失败,用户添加成功。
这里的效果跟REQUIRES_NEW是很像的。
看到这里,可能有人会问:
加入事务和嵌套事务有什么区别?
加入事务 (REQUIRED) 和嵌套事务(NESTED) 都是事务传播机制中的两种传播级别,如果当前不存在事务,那么二者的行为是一致的。但如果当前存在事务:
- 加入事务: 遇到异常时,会回滚全部事务
- 嵌套事务:遇到异常时,回滚部分事务
嵌套事务之所以能回滚部分事务,是因为数据库中存在一个保存点的概念,嵌套事务相对于新建了一个保存点,如果出现异常了,那么只需要回滚到保存点即可,这样就实现了部分事务的回滚。
嵌套事务的实现原理:
嵌套事务之所以能实现部分事务的回滚,是因为在数据库中存在一个保存点(savepoint)的概念,以 MySQL 为例,嵌套事务相当于新建了一个保存点,而滚回时只回滚到当前保存点,因此之前的事务是不受影响的,这一点可以在 MySQL 的官方文档汇总找到相应的资料: https://dev.mysgl.com/doc/refman/5.7/en/savepoint.html
而 REQUIRED 是加入到当前事务中,并没有创建事务的保存点,因此出现了回滚就是整个事务回滚,这就是嵌套事务和加入事务的区别。
保存点就像玩通关游戏时的“游戏存档”一样,如果设置了游戏存档,那么即使当前关卡失败了,也能继续上一个存档点继续玩,而不是从头开始玩游戏。