目录
1.为什么需要事务?
2.MySQL中事务的使用
3.Spring中事务的实现
3.1.编程式事务(手动写代码操作事务)
3.2.声明式事务(利用注解自动开启和提交事务)(使用为主)
3.2.1.@Transactional作用范围
3.2.2.@Transactional参数说明
3.2.3.注意事项:@Transactional + try-catch 有异常不回滚的问题
3.2.4.@Transactional工作原理
4.事务隔离级别
4.1.事务特性
①原子性(Atomicity,或称不可分割性)
②一致性(Consistency)
③隔离性(Isolation,又称独立性)
④持久性(Durability)
4.2.MySQL 事务隔离级别有4种
①READ UNCOMMITTED:读未提交(Read Uncommitted)
②READ COMMITTED:读已提交(Read Committed)
③REPEATABLE READ:可重复读(Repeatable Read)
④SERIALIZABLE:序列化(Serializable)
4.3.Spring事务隔离级别有5种
①Isolation.DEFAULT:以连接的数据库的全局事务隔离级别为主。(default)
②Isolation.READ_UNCOMMITTED:读未提交(=MySQL中的读未提交)(read_uncommitted)
③Isolation.READ_COMMITTED:读已提交(=MySQL中的读已提交)(read_committed)
④Isolation.REPEATABLE_READ:可重复读(=MySQL中的可重复读)(repeatable_read)
⑤Isolation.SERIALIZABLE:串行化(=MySQL中的串行化)(serializable)
4.4.Spring中设置事务隔离级别
5.Spring事务传播机制
5.1.事务传播机制是什么?
5.2.为什么需要事务传播机制?
5.3.事务传播机制有哪些?
①Propagation.REQUIRED:默认的事务传播级别(required)
②Propagation.SUPPORTS(supports)
③Propagation.MANDATORY(mandatory:强制性)
④Propagation.REQUIRES_NEW(requires_new)
⑤Propagation.NOT_SUPPORTED(not_supported)
⑥Propagation.NEVER(never)
⑦Propagation.NESTED(nested)
5.4.Spring事务传播机制使用和各种场景演示
5.4.1.支持当前事务(required)
5.4.2.嵌套事务(nested)
--->PS:嵌套事务(附加事务)(NESTED)和加入事务(REQUIRED )的区别:
6.总结
1.为什么需要事务?
事务定义
将⼀组操作封装成⼀个执⾏单元(封装到⼀起),要么全部成功,要么全部失败。
为什么要⽤事务?
⽐如转账分为两个操作:第⼀步操作:A 账户 -100 元。第⼆步操作:B 账户 +100 元。
如果没有事务,第⼀步执⾏成功了,第⼆步执⾏失败了,那么 A 账户平⽩⽆故的 100 元就“⼈间蒸发”了。⽽如果使⽤事务就可以解决这个问题,让这⼀组操作要么⼀起成功,要么⼀起失败。
(当异常中断了,会将事务的执行过程记录到日志里,当台式电脑断电恢复之后,MySQL重新启动时会首先进行自检,发现某个事务执行了一半,没有结束符,此时会进行补偿机制,业务进行回滚,执行完上次没有执行完的操作,再提交事务。)
2.MySQL中事务的使用
事务在 MySQL 中有 3 个重要的操作:开启事务、提交事务、回滚事务,对应的操作命令如下:
--开启事务
start transaction;
--业务执行
--提交事务
commit;
--回滚事务
rollback;
MySQL中的事务都是单独执行的,不存在Spring中多个事务之间进行传递。
3.Spring中事务的实现
3.1.编程式事务(手动写代码操作事务)
灵活性大,但麻烦。
Spring ⼿动操作事务和上⾯ MySQL 操作事务类似,它也是有 3 个重要操作步骤:
- ①开启事务(获取事务)
- ②提交事务 或 ③回滚事务
SpringBoot 内置了两个对象,DataSourceTransactionManager ⽤来获取事务(开启事务)、提交或回滚事务的;而TransactionDefinition 是事务的属性,在获取事务的时候需要将TransactionDefinition 传递进去从⽽获得⼀个事务TransactionStatus,实现代码如下:
# 配置数据库的连接字符串 spring: datasource: url: jdbc:mysql://127.0.0.1/mycnblog?characterEncoding=utf8 #utf8不支持一些复杂的中文 username: root password: 12345678 driver-class-name: com.mysql.cj.jdbc.Driver #底层驱动的名称(8.0之前版本不加.cj,8.0之后版本加.cj) # 配置mybatis xml的文件路径,在resource包下建立mybatis(其命名自定义)包,在resource/mybatis创建所有表的xml文件 mybatis: mapper-locations: classpath:mapper/**Mapper.xml configuration: # 配置打印MyBatis最终执行的SQL log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 配置打印MyBatis最终执行的SQL logging: level: com: example: demo: debug
保存起来,以后直接用就行。
创建model层下的实体类:
import lombok.Data;
@Data
public class UserInfo {
private int id;
private String username;
private String password;
private String photo;
private String createtime;
private String updatetime;
private String state;
}
创建mapper层下的UserMapper接口写方法声明:
import com.example.demo.model.UserInfo;
import org.apache.ibatis.annotations.Mapper;
@Mapper //此注解一定不要忘记添加
public interface UserMapper {
public int add(UserInfo userInfo);
}
创建mapper层下的UserMapper.xml文件写对应SQL标签:
<?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.example.demo.mapper.UserMapper">
<insert id="add">
insert into userinfo(username, password)
values(#{username}, #{password})
</insert>
</mapper>
创建service层下的UserService类:
import com.example.demo.mapper.UserMapper;
import com.example.demo.model.UserInfo;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
public class UserService {
@Resource
private UserMapper userMapper;
public int add(UserInfo userInfo) {
return userMapper.add(userInfo);
}
}
创建controller层下的UserController类:
import com.example.demo.model.UserInfo;
import com.example.demo.service.UserService;
import lombok.extern.slf4j.Slf4j;
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
@RequestMapping("/user")
@Slf4j
public class UserController {
@Autowired
private UserService userService;
//事务管理器
@Autowired
private DataSourceTransactionManager transactionManager;
//事务属性对象
@Autowired
private TransactionDefinition transactionDefinition;
@RequestMapping("/add")
public int add(UserInfo userInfo) {
//获取事务(开启事务)
TransactionStatus transactionStatus = transactionManager.getTransaction(transactionDefinition);
int result = userService.add(userInfo);
log.info("添加结果:" + result);
// //提交事务
// transactionManager.commit(transactionStatus);
//回滚事务
transactionManager.rollback(transactionStatus);
return result;
}
}
事务回滚成功!若是提交事务,数据库会新增添加的数据。
以上代码虽然可以实现事务,但操作也很繁琐,更简单的实现⽅法——声明式事务。
3.2.声明式事务(利用注解自动开启和提交事务)(使用为主)
简单,但出现问题难解决。
声明式事务只需要在需要的⽅法上添加 @Transactional 注解就可以实现了,⽆需⼿动开启事务和提交事务,进⼊⽅法时自动开启事务,方法成功执行完会自动提交事务,如果中途发生了没有处理的异常会⾃动回滚事务。
(区别:单元测试里只要给要测试的方法上加上@Transactional注解,不管程序怎么执行,最终一定会回滚)
正常情况:
import com.example.demo.model.UserInfo;
import com.example.demo.service.UserService;
import lombok.extern.slf4j.Slf4j;
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("/user")
@Slf4j
public class UserController {
@Autowired
private UserService userService;
@Transactional //开启声明式事务(进入方法自动开启事务,方法正常执行完自动提交事务,如果发生了未处理的异常会自动回滚事务)
@RequestMapping("/add2")
public int add2(UserInfo userInfo) {
int result = userService.add(userInfo);
log.info("添加功能II, 执行结果:" + result);
return result;
}
}
异常情况:
@Transactional
@RequestMapping("/add2")
public int add2(UserInfo userInfo) {
int result = userService.add(userInfo);
log.info("添加功能II, 执行结果:" + result);
int num = 10 / 0; //声明式事务执行到此行发生了未处理的异常会自动回滚事务
return result;
}
3.2.1.@Transactional作用范围
@Transactional 可以⽤来修饰⽅法或类:
- 修饰方法时:需要注意只能应用到 public 方法上,否则不生效。推荐此种⽤法。
- 修饰类时:表明该注解对该类中所有的 public ⽅法都⽣效。
3.2.2.@Transactional参数说明
timeout事务的超时时间:
当遇到问题:
查看对应API文档。
3.2.3.注意事项:@Transactional + try-catch 有异常不回滚的问题
当(在异常被捕获的情况下) @Transactional 遇到try catch之后,即使程序执行一半出现了异常,那么事务也不会⾃动回滚。
原因——Spring设计理念:
- 当程序有异常时,在没有加try catch时,Spring团队会认为当前的程序发生了一个始料未及的意外,开发者没有解决方案,会帮我们自动回滚;
- 在加了try catch后,Spring团队会认为开发者预料到了代码可能会出现异常,认为开发者有责任和能力去解决这个异常,所以此时不会人为干预,会自动提交事务。
import com.example.demo.model.UserInfo;
import com.example.demo.service.UserService;
import lombok.extern.slf4j.Slf4j;
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("/user")
@Slf4j
public class UserController {
@Autowired
private UserService userService;
@Transactional
@RequestMapping("/add2")
public int add2(UserInfo userInfo) throws InterruptedException {
int result = 0;
try {
result = userService.add(userInfo);
log.info("添加功能II, 执行结果:" + result);
int num = 10 / 0;
} catch (Exception e) {
}
return result;
}
}
解决方案1:对于捕获的异常,将异常重新抛出,交给Spring处理,事务就会⾃动回滚了。对前端不友好。一般不用。
@Transactional
@RequestMapping("/add2")
public int add2(UserInfo userInfo) throws InterruptedException {
int result = 0;
try {
result = userService.add(userInfo);
log.info("添加功能II, 执行结果:" + result);
int num = 10 / 0;
} catch (Exception e) {
throw e; //将异常重新抛出去
}
return result;
}
解决方案2:手动回滚事务,在⽅法中使⽤ TransactionAspectSupport.currentTransactionStatus() 可以得到当前的事务,然后设置回滚⽅法 setRollbackOnly 就可以实现回滚了。推荐使用。
import com.example.demo.model.UserInfo;
import com.example.demo.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.interceptor.TransactionAspectSupport;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {
@Autowired
private UserService userService;
@Transactional
@RequestMapping("/add2")
public int add2(UserInfo userInfo) throws InterruptedException {
int result = 0;
try {
result = userService.add(userInfo);
log.info("添加功能II, 执行结果:" + result);
int num = 10 / 0;
} catch (Exception e) {
log.info("程序执行出现异常:" + e.getMessage());
//手动回滚事务
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
return result;
}
}
3.2.4.@Transactional工作原理
@Transactional 是基于 AOP 实现的,AOP ⼜是使⽤动态代理实现的。如果⽬标对象实现了接⼝,默认情况下会采⽤ JDK 的动态代理,如果⽬标对象没有实现了接⼝,会使⽤ CGLIB 动态代理。 @Transactional 在开始执⾏业务之前,通过代理先开启事务,在执⾏成功之后再提交事务。如果中途遇到的异常,则回滚事务。
@Transactional 实现思路预览:
@Transactional 具体执⾏细节如下图所示:
4.事务隔离级别
4.1.事务特性
事务有4 ⼤特性(ACID):原⼦性、持久性、⼀致性、隔离性。
①原子性(Atomicity,或称不可分割性)
⼀个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执⾏过程中发⽣错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执⾏过⼀样。
②一致性(Consistency)
在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写⼊的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以⾃发性地完成预定的⼯作。例如转账操作,张三和李四账户各有50元,张三给李四转账20元,不管是转账之前还是转账之后,他们俩账户的总额一定是和开始一致的。
③隔离性(Isolation,又称独立性)
数据库允许多个并发事务同时对其数据进⾏读写和修改的能⼒,隔离性可以防⽌多个事务并发执⾏时由于交叉执⾏⽽导致数据的不⼀致。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)。
④持久性(Durability)
事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
而这 4 种特性中,只有隔离性(隔离级别)是可以设置的。
为什么要设置事务的隔离级别?
是⽤来保障多个并发事务执⾏更可控,更符合操作者预期的。
什么是可控?
⽐如近⼏年⽐较严重的新冠病毒,我们会把直接接触到确证病例的⼈员隔离到酒店,⽽把间接接触者(和直接接触着但未确诊的⼈)隔离在⾃⼰的家中,也就是针对不同的⼈群,采取不同的隔离级别,这种隔离⽅式就和事务的隔离级别类似,都是采取某种⾏动让某个事件变的“更可控”。而事务的隔离级别就是为了防止,其他的事务影响当前事务执行的一种策略。
4.2.MySQL 事务隔离级别有4种
①READ UNCOMMITTED:读未提交(Read Uncommitted)
也叫未提交读,该隔离级别的事务可以看到其他事务中未提交的数据。该隔离级别因为可以读取到其他事务中未提交的数据,⽽未提交的数据可能会发⽣回滚, 因此我们把该级别读取到的数据称之为脏数据,把这个问题称之为脏读。
②READ COMMITTED:读已提交(Read Committed)
也叫提交读,(是 Oracle 数据库的默认事务隔离级别),该隔离级别的事务能读取到已经提交事务的数据,因此它不会有脏读问题。但由于在事务的执⾏中可以读取到其他事务提交的结果,所以在不同时间的相同 SQL 查询中,可能会得到不同的结果,这种现象叫做不可重复读(描述的是对同一数据的修改)。
③REPEATABLE READ:可重复读(Repeatable Read)
是 MySQL 数据库的默认事务隔离级别,它能确保同⼀事务多次查询的结果⼀致。但也会有新的问题,⽐如此级别的事务正在执⾏时,另⼀个事务成功的插⼊了某条数据,但因为它每次查询的结果都是⼀样的,所以会导致查询不到这条数据,⾃⼰重复插⼊时⼜失败(因为唯⼀约束的原因)。明明在事务中查询不到这条信息,但⾃⼰就是插⼊不进去,这就叫幻读(Phantom Read)(描述的是数据的增加和删除)。
④SERIALIZABLE:序列化(Serializable)
事务最⾼隔离级别,它会强制事务排序,使之不会发生冲突,从⽽解决了脏读、不可重复读和幻读问题,但因为执⾏效率低,所以真正使⽤的场景并不多。
-
脏读:⼀个事务读取到了另⼀个事务修改的数据之后,后⼀个事务⼜进⾏了回滚操作,从⽽导致第⼀个事务读取的数据是错误的。
-
不可重复读:⼀个事务两次查询得到的结果不同,因为在两次查询中间,有另⼀个事务把数据修改了。
-
幻读:⼀个事务两次查询中得到的结果集不同,因为在两次查询中另⼀个事务有新增了⼀部分数据。
在数据库中通过以下 SQL 查询全局事务隔离级别和当前连接的事务隔离级别:
select @@global.tx_isolation,@@tx_isolation;
4.3.Spring事务隔离级别有5种
①Isolation.DEFAULT:以连接的数据库的全局事务隔离级别为主。(default)
②Isolation.READ_UNCOMMITTED:读未提交(=MySQL中的读未提交)(read_uncommitted)
可以读取到未提交的事务,存在脏读。
③Isolation.READ_COMMITTED:读已提交(=MySQL中的读已提交)(read_committed)
只能读取到已经提交的事务,解决了脏读,存在不可重复读。
④Isolation.REPEATABLE_READ:可重复读(=MySQL中的可重复读)(repeatable_read)
解决了不可重复读,但存在幻读(MySQL默认级别)。
⑤Isolation.SERIALIZABLE:串行化(=MySQL中的串行化)(serializable)
可以解决所有并发问题,但性能太低。
相比于 MySQL 的事务隔离级别,Spring 的事务隔离级别只是多了⼀个 Isolation.DEFAULT。
MySQL事务隔离级别与Spring事务隔离级别的区别:
MySQL相当于面条厂商,提供了4种配料;Spring相当于饭店,新增了1种配料。饭店根据客户口味(具体业务)决定要用哪些配料,不用哪些配料。
4.4.Spring中设置事务隔离级别
只需要设置 @Transactional ⾥的 isolation 属性(枚举)即可:
查看源码:
5.Spring事务传播机制
5.1.事务传播机制是什么?
Spring 事务传播机制定义了多个包含了事务的方法(就相当于MySQL里的事务),相互调用时,事务是如何在这些方法间进行传递的。
5.2.为什么需要事务传播机制?
事务隔离级别是保证多个并发事务执⾏的可控性的(稳定性),⽽事务传播机制是保证“一个”事务在多个调用方法间的可控性的(稳定性)。
例⼦:像新冠病毒⼀样,它有不同的隔离⽅式(酒店隔离&居家隔离),是为了保证疫情可控,然⽽在每个⼈的隔离过程中,会有很多个执⾏的环节,⽐如酒店隔离,需要负责⼈员运送、物品运送、 消杀原⽣活区域、定时核算检查和定时送餐等很多环节,而事务传播机制就是保证⼀个事务在传递过程中是可靠性的,回到本身案例中就是保证每个⼈在隔离的过程中可控的。
事务隔离级别解决的是多个事务同时调⽤⼀个数据库的问题:
而事务传播机制解决的是⼀个事务在多个节点(⽅法)中传递的问题:
路途越远出现问题的概率就越大。
5.3.事务传播机制有哪些?
①Propagation.REQUIRED:默认的事务传播级别(required)
它表示如果当前存在事务,则加入该事务;如果当前没有事务,则创建⼀个新的事务。(A方法需要去调用B方法,此时B方法的行为:会判断A方法是否有事务,若存在事务,把当前的B方法加入到A方法中)
②Propagation.SUPPORTS(supports)
如果当前存在事务,则加⼊该事务;如果当前没有事务,则以非事务的方式继续运⾏。
③Propagation.MANDATORY(mandatory:强制性)
如果当前存在事务,则加⼊该事务;如果当前没有事务,则抛出异常。
④Propagation.REQUIRES_NEW(requires_new)
表示创建⼀个新的事务,如果当前存在事务,则把当前事务挂起。也就是说不管外部⽅法是否开启事务,Propagation.REQUIRES_NEW 修饰的内部⽅法会新开启⾃⼰的事务,且开启的事务相互独⽴,互不⼲扰。
⑤Propagation.NOT_SUPPORTED(not_supported)
以⾮事务⽅式运⾏,如果当前存在事务,则把当前事务挂起。
⑥Propagation.NEVER(never)
以⾮事务⽅式运⾏,如果当前存在事务,则抛出异常。
⑦Propagation.NESTED(nested)
如果当前存在事务,则创建⼀个事务作为当前事务的嵌套事务来运⾏;如果当前没有事务,则该取值等价于PROPAGATION_REQUIRED。
以上 7 种传播⾏为,可以根据是否⽀持当前事务分为以下 3 类:
以情侣关系为例来理解以上分类:
5.4.Spring事务传播机制使用和各种场景演示
怎样设置事务的传播机制?
只需要设置 @Transactional ⾥的 propagation属性(枚举)即可:
支持多个参数设置:
5.4.1.支持当前事务(required)
创建日志的实体类LogInfo
import lombok.Data;
@Data
public class LogInfo {
private int id;
private String name;
private String desc;
private String createtime;
}
在日志表的数据持久化层mapper下创建LogMapper接口:
import com.example.demo.model.LogInfo;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface LogMapper {
int add(LogInfo logInfo);
}
创建LogMapper.xml文件:
<?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.example.demo.mapper.LogMapper">
<insert id="add">
insert into loginfo(`name`,`desc`)
values(#{name},#{desc})
</insert>
</mapper>
创建LogService类:
import com.example.demo.mapper.LogMapper;
import com.example.demo.model.LogInfo;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
@Service
public class LogService {
@Resource
private LogMapper logMapper;
@Transactional(propagation = Propagation.REQUIRED) //默认的,设置不设置都一样
public int add(LogInfo logInfo) {
int num = 10 / 0;
return logMapper.add(logInfo);
}
}
import com.example.demo.mapper.UserMapper;
import com.example.demo.model.UserInfo;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
@Service
public class UserService {
@Resource
private UserMapper userMapper;
@Transactional(propagation = Propagation.REQUIRED)
public int add(UserInfo userInfo) {
return userMapper.add(userInfo);
}
}
import com.example.demo.model.LogInfo;
import com.example.demo.model.UserInfo;
import com.example.demo.service.LogService;
import com.example.demo.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {
@Autowired
private UserService userService;
@Autowired
private LogService logService;
@RequestMapping("/add3")
@Transactional(propagation = Propagation.REQUIRED)
public int add3(UserInfo userInfo) {
//非空效验
if(userInfo == null || !StringUtils.hasLength(userInfo.getUsername()) || !StringUtils.hasLength(userInfo.getPassword())) {
return -1; //非法参数
}
int result = 0;
//添加用户
int resultByUser = userService.add(userInfo);
log.info("添加用户的结果:" + resultByUser);
//添加日志
LogInfo logInfo = new LogInfo();
logInfo.setName("添加用户");
logInfo.setDesc("用户信息:" + userInfo);
int resultByLog = logService.add(logInfo);
log.info("添加日志的结果:" + resultByLog);
if(resultByUser == 1 && resultByLog == 1) result = 1;
return result;
}
}
执行结果:数据库没有插入任何数据。
执行流程:
-
UserService类中的add方法正常执行完成。
-
LogService类中的add方法发生异常,执行报错。
-
UserController有事务为REQUIRED,则userService.add()和logService.add()这两个方法都会加入该事务REQUIRED,当LogService类中的add方法发生异常而未做try-catch处理时,会导致整个程序发生异常。UserController类中的add3方法为了求稳,会将整个事务都会回滚,结果是数据库中loginfo表和userinfo表都没有添加任何数据。【事务对错的执行机制是优于事务的传播机制的】
给LogService类中的add方法里加上try-catch:
import com.example.demo.mapper.LogMapper;
import com.example.demo.model.LogInfo;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.interceptor.TransactionAspectSupport;
import javax.annotation.Resource;
@Service
public class LogService {
@Resource
private LogMapper logMapper;
@Transactional(propagation = Propagation.REQUIRED)
public int add(LogInfo logInfo) {
int result = logMapper.add(logInfo);
try{
int num = 10 / 0;
} catch (Exception e) {
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
result = 0;
}
return result;
}
}
执行结果和上面一样。
执行流程:
-
UserService类中的add方法正常执行完成。
-
LogService类中的add方法发生异常,执行报错。
-
UserController有事务为REQUIRED,则userService.add()和logService.add()这两个方法都会加入该事务REQUIRED,当LogService类中的add方法发生异常并做了try-catch处理时,一损俱损,一荣俱荣,整个事务都会回滚,结果是数据库中loginfo表和userinfo表都没有添加任何数据。
5.4.2.嵌套事务(nested)
@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {
@Autowired
private UserService userService;
@Autowired
private LogService logService;
@RequestMapping("/add3")
@Transactional(propagation = Propagation.NESTED)
public int add3(UserInfo userInfo) {
//非空效验
if(userInfo == null || !StringUtils.hasLength(userInfo.getUsername()) || !StringUtils.hasLength(userInfo.getPassword())) {
return -1; //非法参数
}
int result = 0;
//添加用户
int resultByUser = userService.add(userInfo);
log.info("添加用户的结果:" + resultByUser);
//添加日志
LogInfo logInfo = new LogInfo();
logInfo.setName("添加用户");
logInfo.setDesc("用户信息:" + userInfo);
int resultByLog = logService.add(logInfo);
log.info("添加日志的结果:" + resultByLog);
if(resultByUser == 1 && resultByLog == 1) result = 1;
return result;
}
}
@Service
public class UserService {
@Resource
private UserMapper userMapper;
@Transactional(propagation = Propagation.NESTED)
public int add(UserInfo userInfo) {
return userMapper.add(userInfo);
}
}
@Service
public class LogService {
@Resource
private LogMapper logMapper;
@Transactional(propagation = Propagation.NESTED) //默认的,设置不设置都一样
public int add(LogInfo logInfo) {
int num = 10 / 0;
return logMapper.add(logInfo);
}
}
执行结果:数据库中没有添加任何数据。
执行流程:
-
UserService类中的add方法正常执行完成。
-
LogService类中的add方法发生异常,执行报错。
-
UserController没有事务,创建事务为NESTED,则userService.add()和logService.add()这两个方法的事务也是NESTED,当LogService类中的add方法发生异常而未做try-catch处理时,logService.add()作为UserController类中的add3方法中的一部分,导致大方法add3中的某一个节点出现异常,导致整个程序发生异常,事务为了求稳,会将整个大的事务都进行回滚,结果是数据库中loginfo表和userinfo表都没有添加任何数据。【即使是NESTED,也是将整个事务进行回滚,原因是当检测到事务里的方法出错时,是不看事务传播机制的,优先走了异常的回滚,而没有走事务的传播机制,事务对错的执行机制是优于事务的传播机制的。当事务没有报错时,某一个节点出现异常,是以事务传播机制为准的】
给LogService类中的add方法里加上try-catch:
@Service
public class LogService {
@Resource
private LogMapper logMapper;
@Transactional(propagation = Propagation.NESTED)
public int add(LogInfo logInfo) {
int result = logMapper.add(logInfo);
try{
int num = 10 / 0;
} catch (Exception e) {
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
result = 0;
}
return result;
}
}
执行结果:数据库中loginfo表没有添加数据,userinfo表添加了数据。
执行流程:
-
UserService类中的add方法正常执行完成。
-
LogService类中的add方法发生异常,执行报错。
-
UserController没有事务,创建事务为NESTED,则userService.add()和logService.add()这两个方法的事务也是NESTED,当LogService类中的add方法发生异常并做了try-catch处理时,只需要将logService.add()进行回滚,整个事务本身没有进行回滚,所以userService.add()正常执行,结果是数据库中loginfo表没有添加数据,userinfo表添加了数据。
--->PS:嵌套事务(附加事务)(NESTED)和加入事务(REQUIRED )的区别:
加入是让自己成为其中的一部分,如果自己出现问题,那么整个事务都会出现问题。
嵌套语义更浅,让自己尝试加进去,如果自己出现问题,就把嵌套进来的自己干掉,但是整个事务在去执行时是不影响的。
整个事务如果全部执⾏成功,⼆者的结果是⼀样的。
如果事务执⾏到⼀半失败了,那么加入事务整个事务会全部回滚,不能实现部分事务的回滚;⽽嵌套事务会局部回滚,不会影响上⼀个⽅法中执⾏的结果。
嵌套事务之所以能够实现部分事务的回滚,是因为事务中有⼀个保存点(savepoint)的概念,嵌套事务进⼊之后相当于新建了⼀个保存点,⽽回滚时只回滚到当前保存点,因此之前的事务是不受影响的,这⼀点可以在 MySQL 的官⽅⽂档汇总找到相应的资料:
MySQL官方文档https://dev.mysql.com/doc/refman/5.7/en/savepoint.html⽽ REQUIRED 是加⼊到当前事务中,并没有创建事务的保存点,因此出现了回滚就是整个事务回滚,这就是嵌套事务和加⼊事务的区别。
6.总结
-
在 Spring 项⽬中使⽤事务,⽤两种⽅法⼿动操作和声明式⾃动提交,其中后者使⽤的最多,在⽅法上添加 @Transactional 就可以实现了。
-
设置事务的隔离级别 @Transactional(isolation = Isolation.SERIALIZABLE),Spring 中的事务隔离级别有 5 种。
-
设置事务的传播机制 @Transactional(propagation = Propagation.REQUIRED),Spring 中的事务传播级别有 7 种。