Spring事务与事务传播机制
场景引入:
在程序开发的时候,我们会遇到这样的一个场景:转账
A转账给B
A账户:-100(扣款)
B账户:金额不变(未到账)
未到账原因可能是数据库断开连接,主机崩溃等等。
此时,如若不做其他处理,那么A就是白白丢失了这100块钱
所以,为了解决这个问题,那么就引入事务了。
事务:
对于事务这块,小编的的MySQL专栏中的事务文章,做了相对详细的分享,有兴趣的可以回头看看。
那么在这里呢,就简单解释下
事务是一组操作的集合,是一个不可分割的操作。
事务会把所有的操作作为一个整体,一起向数据库提交或是撤销数据库请求,所以这组操作要么同时完成,要么同时失败。
事务操作(MySQL)
在MySQL中,事务的操作主要是分为三步
1.开启事务(一组操作前开启事务)
2.提交事务(这组操作全部成功,提交事务)
3.回滚事务(这组操作中间任何一个操作出现异常,回滚事务)
--开启事务
start transaction;
--提交事务
commit;
--回滚事务
rollback;以上是展示MySQL如何操作的
以下是在Spring中展示Spring如何操作
事务操作(Spring)
一:项目准备
1.创建一个Springboot项目
2.创建以下表:
-- 创建数据库
DROP DATABASE IF EXISTS trans_test;
CREATE DATABASE trans_test DEFAULT CHARACTER SET utf8mb4;
--用户表
DROP TABLE IF EXISTS user_info;
CREATE TABLE user_info (
`id` INT NOT NULL AUTO_INCREMENT,
`user_name` VARCHAR(128) NOT NULL,
`password` VARCHAR(128) NOT NULL,
`create_time` DATETIME DEFAULT now(),
`update_time` DATETIME DEFAULT now() ON UPDATE now(),
PRIMARY KEY (`id`)
) ENGINE = INNODB DEFAULT CHARACTER SET = utf8mb4 COMMENT = '用户表';
-- 操作日志表
-- DROP TABLE IF EXISTS log_info;
CREATE TABLE log_info (
`id` INT PRIMARY KEY auto_increment,
`user_name` VARCHAR(128) NOT NULL,
`op` VARCHAR(256) NOT NULL,
`create_time` DATETIME DEFAULT now(),
`update_time` DATETIME DEFAULT now() ON UPDATE now()
) DEFAULT charset 'utf8mb4';3.创建model包,并创建以下类
这两个类是暂时没有用到。
@Data
public class LogInfo {
private int id;
private String userName;
private String op;
private Date createTime;
private Date updateTime;
}@Data
public class UserInfo {
private int id;
private String userName;
private String password;
private Date createTime;
private Date updateTime;
}4.创建mapper层,并创建以下类
@Mapper
public interface LogInfoMapper {
@Insert("insert into log_info(user_name,op) " +
"values(#{userName},#{op})")
Integer createLogInfo(String userName,String op);
}@Mapper
public interface UserInfoMapper {
@Insert("insert into user_info(user_name,password) values" +
"(#{userName},#{password})")
Integer register(String userName,String password);
}5.创建Service层,并创建以下类
@Service
public class UserInfoService {
@Autowired
private UserInfoMapper mapper;
public int insert(String username,String password){
return mapper.register(username,password);
}
}@Service
public class LogInfoService {
@Autowired
private LogInfoMapper mapper;
public int insertLog(String username, String op) {
Integer ret = mapper.createLogInfo(username, op);
return ret;
}
}
6.创建controller层,并创建以下类
@RestController
@RequestMapping("/log")
public class LogInfoController {
@Autowired
private LogInfoService service;
public String insertLog(String username,String op){
service.insertLog(username,op);
return "插入日志成功!";
}
}二:演示事务
在spring中,提供了三种方式供我们使用事务使用
1.编程式事务
2.基于注解的声明式事务
3.基于XML的声明式事务
本文演示前两种,第三种有兴趣可以网上再去看看。
编程式事务:
在controller层,创建以下类:
@Slf4j
@RestController
@RequestMapping("/user")
public class UserInfoController {
@Autowired
private UserInfoService service;
//以下使用代码方式来提交事务
//获取一个事务管理对象
@Autowired
private DataSourceTransactionManager manager;
//获取一个事务定义
@Autowired
private TransactionDefinition definition;
@RequestMapping("u1")
public String register(String username,String password){
//开启事务
TransactionStatus status=manager.getTransaction(definition);
service.insert(username,password);
//提交事务
manager.commit(status);
return "注册成功!";
}
}DataSourceTransactionManager 、TransactionDefinition 这两个对象,Spring帮我们管理好了
直接注入即可
创建一个管理对象以及一个事务定义,是为了显式去控制事务
允许程序通过postman测试接口
结果返回:



值得注意的是,如若发生异常,即代码中有运行时异常,那么此时事务就会提交失败
那么出现失败的话,此时我们应该进行回滚,下面演示出错了如何回滚
@Slf4j
@RestController
@RequestMapping("/user")
public class UserInfoController {
@Autowired
private UserInfoService service;
//以下使用代码方式来提交事务
//获取一个事务管理对象
@Autowired
private DataSourceTransactionManager manager;
//获取一个事务定义
@Autowired
private TransactionDefinition definition;
@RequestMapping("u1")
public String register(String username,String password){
//开启事务
TransactionStatus status=manager.getTransaction(definition);
service.insert(username,password);
try{
int ret=10/0;
}catch (Exception e){
log.info("插入出错!");
//事务回滚
manager.rollback(status);
}
//提交事务
manager.commit(status);
return "注册成功!";
}
}通过postman测试同样接口,返回结果如下:


数据库并没有数据,但插入语句确实执行了,那么在navicat中右键user_info的设计表中可以看到此结果

对于编程式事务,就分享到这里
基于注解式事务
在controller包中创建该TransactionController类
@Transactional注解介绍:
它是 Spring 提供的用于 声明式事务管理 的注解,用于在方法或类上标注,让 Spring 自动在方法执行前开启事务,在方法执行成功后提交事务,如果方法抛出异常,则回滚事务。
表格 还在加载中,请等待加载完成后再尝试复制
使用注解(无异常):
@Slf4j
@RestController
@RequestMapping("/user2")
public class TransactionController {
@Autowired
private UserInfoService service;
//以下使用注解来使用事务
/**
* 情况一:使用注解,无异常情况下,数据插入成功
* @param username
* @param password
* @return
*/
@Transactional
@RequestMapping("/u1")
public String r1(String username,String password){
service.insert(username,password);
return "注册成功!";
}
}访问该接口,这里及其后面给出url:http://127.0.0.1:8080/user2/u1,不给postman截图


使用注解(异常)
/**
* 添加事务(代码出错),事务自动回滚
* @param username
* @param password
* @return
*/
@Transactional
@RequestMapping("/u2")
public String r2(String username,String password){
int ret = service.insert(username, password);
if(ret==1){
log.info("插入成功,影响行数:"+ret);
}
int temp=10/0;
return "注册成功!";
}此时,当代码出现运行时异常的时候,该注解回滚代码
访问url:http://127.0.0.1:8080/user2/u2


此时插入代码执行了,所以该表ID也是自动增加了

使用catch语句处理
/**
* 出错语句被catch住,那么事务不会回滚
* @param username
* @param password
* @return
*/
@Transactional
@RequestMapping("/u3")
public String r3(String username,String password){
int ret = service.insert(username, password);
if(ret==1){
log.info("插入成功,影响行数:"+ret);
}
try {
int temp=10/0;
} catch (Exception e) {
log.info("运行出错,事务不回滚");
}
return "注册成功!";
}访问URL:http://127.0.0.1:8080/user2/u3


使用catch语句(仍抛出异常)
/**
* 事务被catch住,但抛出后运行时异常,事务回滚
* @param username
* @param password
* @return
*/
@Transactional
@RequestMapping("/u4")
public String r4(String username,String password){
int ret = service.insert(username, password);
if(ret==1){
log.info("插入成功,影响行数:"+ret);
}
try {
int temp=10/0;
} catch (Exception e) {
log.info("运行出错,事务不回滚");
throw e;
}
return "注册成功!";
}访问URL:http://127.0.0.1:8080/user2/u4


按照顺序而言,此时ID自增为6,在执行玩插入代码后
catch也不是限定死的,即使是被catch住,也可以进行事务强制回滚
使用catch语句(强制回滚)
/**
* 既然抛出异常了,catch住,仍然可以设置回滚
* @param username
* @param password
* @return
*/
@Transactional
@RequestMapping("/u5")
public String r5(String username,String password){
int ret = service.insert(username, password);
if(ret==1){
log.info("插入成功,影响行数:"+ret);
}
try {
int temp=10/0;
} catch (Exception e) {
log.info("运行出错,事务不回滚");
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
return "注册成功!";
}访问URL:http://127.0.0.1:8080/user2/u5


此时,执行完插入语句后,该ID自增为7
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
在当前事务中,手动标记该事务只能回滚,即使没有抛出异常,Spring 也会在方法执行完后回滚事务,而不是提交。
必须在 事务方法内部调用才有效,否则会抛出 NoTransactionException。
不能用于非事务方法或非事务线程中。
抛出非运行时异常
/**
*抛出非运行时异常,事务仍然会提交
* @param username
* @param password
* @return
*/
@Transactional
@RequestMapping("/u6")
public String r6(String username,String password) throws IOException {
int ret = service.insert(username, password);
if(ret==1){
log.info("插入成功,影响行数:"+ret);
}
if(true){
throw new IOException();
}
return "注册成功!";
}

所以说明的是,该事务注解出现异常回滚时是对运行时异常有效
对于非运行时异常而言,也可以通过该设置@Transactional注解中的rollbackFor值来回滚事务
抛出非运行时异常(设置回滚)
/**
* 通过设置rollbackFor,非运行时异常也可以回滚
* @param username
* @param password
* @return
* @throws IOException
*/
@Transactional(rollbackFor = Exception.class)
@RequestMapping("/u7")
public String r7(String username,String password) throws IOException {
int ret = service.insert(username, password);
if(ret==1){
log.info("插入成功,影响行数:"+ret);
}
if(true){
throw new IOException();
}
return "注册成功!";
}访问URL:http://127.0.0.1:8080/user2/u7


此时ID自增为9
只要代码抛出的异常属于rollbackFor = Exception.class中的值的子类活着是本身,都可以进行回滚
如若是rollbackFor = ?设置的值是代码抛出异常的子类,即rollbackFor = ?是范围小,代码抛出的异常是范围大
那么情况又是如何?
抛出非运行时异常(设定范围)
/**
* 在非抛出非运行时异常的情况下,设定小范围异常,抛出大范围异常
* 事务不回滚
* @param username
* @param password
* @return
* @throws IOException
*/
@Transactional(rollbackFor = JSException.class)
@RequestMapping("/u8")
public String r8(String username,String password) throws IOException {
int ret = service.insert(username, password);
if(ret==1){
log.info("插入成功,影响行数:"+ret);
}
if(true){
throw new IOException();
}
return "注册成功!";
}访问URL:http://127.0.0.1:8080/user2/u8


设定事务的隔离级别
对于事务的隔离级别相对详细解释,请看这个小编另一个文章:https://blog.csdn.net/m0_75169015/article/details/143476687
那么对应到Spring中的事务隔离级别,有以下:
Isolaolation.DEFAULT:以连接的数据的事务隔离级别为主(MySQL为读已提交)
Isolaolation.READ_UNCOMMTIED:读未提交,对应SQL标准中READ UNCOMMTIED
Isolaolation.READ_COMMTIED:读已提交,对应SQL标准中READ COMMTIED
Isolaolation.REPEATABLE_READ:可重复读,对应SQL标准中REPEATABLE READ
Isolaolation.SERIALIZABLE:串行化,对应SQL标准中SERIALIZABLE
举例代码,在TransactionController类中,插入以下代码片段:
/**
* 设置事务的隔离级别,mysql默认是读已提交
* @param username
* @param password
* @return
* @throws IOException
*/
@Transactional(isolation = Isolation.DEFAULT)
@RequestMapping("/u9")
public String r9(String username,String password) throws IOException {
int ret = service.insert(username, password);
if(ret==1){
log.info("插入成功,影响行数:"+ret);
}
if(true){
throw new IOException();
}
return "注册成功!";
}
}访问URL:http://127.0.0.1:8080/user2/u9


以上呢,是对你会发现有些是事务失效了,有些是事务生效了,那么对于事务失效这块而言,原因上面提到的
是不多的,下面就给出对于事务失效常见原因
事务失效:
1.方法不是public
Spring AOP 默认只拦截 public 方法,protected/private 的方法不会被代理。
2.内部方法调用(this调用自己)
一个带 @Transactional 的方法如果被同类的另一个方法直接调用,是不会经过代理对象的,事务不会生效。
因为,这个@Transactional是基于SpringAOP实现的,那么对于SpringAOP只会对生成代理对象调用的方法进行增强
并不会拦截内部this调用的方法
代码示例:
public void outer() {
this.inner(); // 不生效
}
@Transactional
public void inner() {
// 不会被代理拦截
}
解决办法:
通过代理对象调用对应方法
@Service
public class UserService {
@Autowired
private UserService self; // 注意:Spring 会注入代理对象
public void methodA() {
self.methodB(); // 调用的是代理对象的方法,事务生效!
}
@Transactional
public void methodB() {
...
}
}
抽取到另一个类中调用
@Service
public class InnerService {
@Transactional
public void methodB() {
...
}
}
@Service
public class UserService {
@Autowired
private InnerService inner;
public void methodA() {
inner.methodB(); // 生效!
}
}
3.异常被catch掉,没有手动回滚
spring事务默认只对方法中抛出未捕获的RuntimeException才会回滚
具体代码,演示事务中已体现
4. 抛出的不是 RuntimeException 且未设置 rollbackFor
5.多线程/异步操作中事务失效
代码举例:
@Transactional
public void asyncSave() {
new Thread(() -> {
repository.save(...); // 无事务保护
}).start();
}
这样是失效的,原因简单解释:
Spring 的事务管理是基于 ThreadLocal 的。
ThreadLocal 是什么?
它是一个线程私有的变量,Spring 会这样维护事务:
// 概念性伪代码
ThreadLocal<TransactionStatus> threadLocal = new ThreadLocal<>();
事务状态是绑定在当前线程上的,新线程不会共享老线程的 ThreadLocal 值。
所以你开启一个新线程后,Spring 就再也找不到原来的事务上下文,自然也就无法参与事务管理。
解决方法:
1.不手动开启线程,用@Async
@Service
public class UserService {
@Async
@Transactional
public void asyncSave() {
repository.save(...); // 事务生效
}
}
@SpringBootApplication
@EnableAsync
public class MyApp {
}
使用 @Async 会让 Spring 管理线程池;
Spring 能在这个线程中创建自己的代理对象(有事务、有上下文);
所以 @Transactional 可以生效。
2.在线程内部使用编程式事务
3.把对应操作放进主线程中执行
事务传播机制
用于定义 当一个事务方法调用另一个事务方法时,事务该如何传播(处理)。
简单而言,就是A方法有事务,B方法是有事务,A调用B时,B事务是加入A中,还是新建一个事务。
那么这个事务传播机制是通过@Transactional中的propagation属性设置的。
A调用B,已B为基准
事务传播机制种类:
Propagation.REQUIRED
默认的事务传播机制,如果当前存在事务,则加入该事务,如果当前没有事务,则创建一个新事务
Propagation.SUPPORTS
如若当前存在事务,则加入该事务,如若当前没有事务,则以非事务方式运行
Propagation.MANDATORY
强制性,如若当前存在事务,则加入该事务,如若当前没有事务,则抛出异常
Propagation.REQUIRES_NEW
创建一个新的事务,如若当前存在事务,则把当前事务挂起,也就是说不管外部方法事务是否开启,当前方法设置该属性后,该方法都会新开启自己的事务,且开启的事务相互独立,互不干扰。
Propagation.NOT_SUPPORTS
以非事务的方式运行,如果当前存在事务,则把当前事务挂起
Propagation.NEVER
以非事务方式运行,如若存在事务,则抛出异常
Propagation.NESTED
如果当前存在事务,则创建一个事务作为当前事务来运行,如若没有事务,该取值等同Propagation.REQUIRED
下面通过代码演示四个情况:
情况皆以A当前存在事务,B也存在事务
代码准备:
controller层创建该类
@RestController
@RequestMapping("/propagation")
public class propagationController {
@Autowired
private LogInfoService logInfoService;
@Autowired
private UserInfoService userInfoService;
//以下演示为事务传播机制
//controller加事务是不合规范的
@Transactional
@RequestMapping("/p1")
public String p1(String username,String password){
userInfoService.insert(username,password);
try {
logInfoService.insertLog(username,"用户注册");
}catch (Exception e){
System.out.println("子事务异常被捕获"+e.getMessage());
}
return "注册/日志插入成功";
}
}
service层修改UserInfoService
@Service
public class UserInfoService {
@Autowired
private UserInfoMapper mapper;
@Transactional
public int insert(String username,String password){
return mapper.register(username,password);
}
}Propagation.REQUIRED
在service层中的修改该类,内容修改后如下
@Service
public class LogInfoService {
@Autowired
private LogInfoMapper mapper;
//演示事务传播机制Propagation.REQUIRED
@Transactional(propagation = Propagation.REQUIRED)
public int insertLog(String username,String op){
return mapper.createLogInfo(username, op);
}
} 访问URL:http://127.0.0.1:8080/propagation/p1,依旧通过postman测试


Propagation.REQUIRES_NEW
该LogInfoService 类下添加:
//演示事务传播机制Propagation.REQUIRES_NEW
@Transactional(propagation = Propagation.REQUIRES_NEW)
public int insertLog(String username, String op) {
Integer ret = mapper.createLogInfo(username, op);
int temp = 10 / 0;
return ret;
}访问URL:http://127.0.0.1:8080/propagation/p1



由于是logservice中的@Transactional是设置了该Propagation.REQUIRES_NEW
所以开启了一个新事务,新事务与其他事务独立,此时logservice遇到异常了,所以只会是logservice中事务回滚,不涉及到userservice
也不涉及到propagationController中回滚
Propagation.NEVER
//演示事务传播机制Propagation.NEVER
@Transactional(propagation = Propagation.NEVER)
public int insertLog(String username, String op) {
Integer ret = mapper.createLogInfo(username, op);
return ret;
}访问URL:http://127.0.0.1:8080/propagation/p1


显然此时发现控制台中,只有用户注册成功,日志记录没有插入成功。
Propagation.NESTED
//演示事务传播机制Propagation.NESTED
@Transactional(propagation = Propagation.NESTED)
public int insertLog(String username, String op) {
Integer ret = mapper.createLogInfo(username, op);
int temp = 10 / 0;
return ret;
}访问URL:http://127.0.0.1:8080/propagation/p1



此时由于设置了机制为NESTED,所以回滚的也只是logservice中方法,主事务即controller中的方法
并不会回滚。
那么如若propagationController没有把异常进行捕获,此时主事务和嵌套事务将会一起回滚。
对于NESTED中,嵌套事务是如何回滚的,这里简单说下
NESTED 的本质:使用保存点 Savepoint 回滚子流程
只要主事务还没提交,你可以把某一部分失败的子逻辑 “撤回” 到某个保存点,而不影响主流程。
就像游戏里你打副本,存了个档,发现这一波走错了,load 回保存点再继续打。
以上就是小编分享Spring中事务一些简单事项