🌸个人主页:https://blog.csdn.net/2301_80050796?spm=1000.2115.3001.5343
🏵️热门专栏:
🧊 Java基本语法(97平均质量分)https://blog.csdn.net/2301_80050796/category_12615970.html?spm=1001.2014.3001.5482
🍕 Collection与数据结构 (92平均质量分)https://blog.csdn.net/2301_80050796/category_12621348.html?spm=1001.2014.3001.5482
🧀线程与网络(96平均质量分) https://blog.csdn.net/2301_80050796/category_12643370.html?spm=1001.2014.3001.5482
🍭MySql数据库(93平均质量分)https://blog.csdn.net/2301_80050796/category_12629890.html?spm=1001.2014.3001.5482
🍬算法(97平均质量分)https://blog.csdn.net/2301_80050796/category_12676091.html?spm=1001.2014.3001.5482
🍃 Spring(97平均质量分)https://blog.csdn.net/2301_80050796/category_12724152.html?spm=1001.2014.3001.5482
感谢点赞与关注~~~
目录
- 1. 回忆数据库中的事务
- 2. Spring中事务的实现
- 2.1 Spring编程式事务
- 2.2 Spring声明式事务@Transactional
- 2.3 @Transactional的作用
- 3. @Transactional详解
- 3.1 rollbackFor
- 3.2 事务隔离级别
- 3.2.1 回顾MySQL中的事务隔离级别
- 3.3.2 Spring事务隔离级别
- 3.3 Spring事务传播机制
- 3.3.1 概念
- 3.3.2 事务的传播机制分类
- 3.3.3 Spring事务传播机制代码演示
1. 回忆数据库中的事务
https://lilesily12385.blog.csdn.net/article/details/137935719
2. Spring中事务的实现
Spring中的事务操作分为两类:
- 编程式事务(手动写代码操作事务)
- 声明式事务(使用注解完成)
现有一需求: 用户注册,注册时候在日志表中插入一条操作记录
数据准备:
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';
代码准备:
1. 创建项目,配置文件引入日志打印,驼峰转换,数据库配置等.
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/trans_test?
characterEncoding=utf8&useSSL=false
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
configuration: # 配置打印 MyBatis⽇志
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
map-underscore-to-camel-case: true #配置驼峰⾃动转换
创建实体类:
import lombok.Data;
import java.util.Date;
@Data
public class UserInfo {
private Integer id;
private String userName;
private String password;
private Date createTime;
private Date updateTime;
}
mport lombok.Data;
import java.util.Date;
@Data
public class LogInfo {
private Integer id;
private String userName;
private String op;
private Date createTime;
private Date updateTime;
}
Mapper:
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserInfoMapper {
@Insert("insert into user_info(`user_name`,`password`)values(#{name},#{password})")
Integer insert(String name,String password);
}
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface LogInfoMapper {
@Insert("insert into log_info(`user_name`,`op`)values(#{name},#{op})")
Integer insertLog(String name,String op);
}
Service:
@Slf4j
@Service
public class UserService {
@Autowired
private UserInfoMapper userInfoMapper;
public void registryUser(String name,String password){
//插⼊⽤⼾信息
userInfoMapper.insert(name,password);
}
}
@Slf4j
@Service
public class LogService {
@Autowired
private LogInfoMapper logInfoMapper;
public void insertLog(String name,String op){
//记录⽤⼾操作
logInfoMapper.insertLog(name,"⽤⼾注册");
}
}
2.1 Spring编程式事务
Spring手动操作事务主要分为三步,与Sql事务的操作类似:
- 开启事务
- 提交事务
- 或者回滚事务
@RestController
@RequestMapping("/user")
public class TransactionController {
@Autowired
private UserService userService;
@Autowired
private DataSourceTransactionManager dataSourceTransactionManager;
@Autowired
private TransactionDefinition transactionDefinition;
@RequestMapping("/registry")
public String login(String userName,String password){
//开启事务
TransactionStatus transaction = dataSourceTransactionManager.getTransaction(transactionDefinition);
//用户注册
userService.registryUser(userName,password);
//提交事务
dataSourceTransactionManager.commit(transaction);
//回滚事务
// dataSourceTransactionManager.rollback(transaction);
return "注册成功";
}
}
SpringBoot内置了两个对象:
DataSourceTransactionManager
,数据源事务管理器,一般用于事务的开启回滚和提交.TransactionDefinition
是事务的属性,一般用于传给事务管理器的getTransaction
方法,用于事务的获取与开启.
- 观察事务提交
dataSourceTransactionManager.commit(transaction);
我们观察到数据库,数据被插入成功.
- 观察事务回滚
dataSourceTransactionManager.rollback(transaction);
我们看到虽然返回的结果是注册成功,但是数据库中并没有多出任何数据.
下面我们来学习一种简单而快捷的方法,使用注解声明.
2.2 Spring声明式事务@Transactional
一共有两部操作:
- 添加依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
</dependency>
- 在事务的方法上添加
@Transactional
注解就可以实现了.无需手动开启和提交事务,在事务全部执行完成之后会自动提交,在中途发生异常的手会自动回滚.
@RestController
@RequestMapping("/user2")
public class TransactionController2 {
@Autowired
private UserService userService;
@RequestMapping("/registry2")
@Transactional
public String registry2(String userName,String password){
userService.registryUser(userName,password);
return "注册成功";
}
}
运行程序后,数据插入成功.
修改程序,使之出现异常:
@RestController
@RequestMapping("/user2")
public class TransactionController2 {
@Autowired
private UserService userService;
@RequestMapping("/registry2")
@Transactional
public String registry2(String userName,String password){
userService.registryUser(userName,password);
int i = 10/0;
return "注册成功";
}
}
运行之后,虽然返回了注册成功,但是数据库并没有更新结果.
我们一般写事务的时候会在业务逻辑层来控制事务,因为在业务逻层中,一个业务功能可能会包含多个数据库访问操作,这样就可以把多个访问数据库的操作 合并在同一个事务中.
2.3 @Transactional的作用
@Transactional
可以用来修饰方法或者是类.
修饰方法的时候,只对public修饰的方法生效,修饰其他方法也不会报错,但是也不会生效.
修饰类的时候,对类中的所有public方法生效.
在程序出现异常的时候,如果异常没有被捕获,这时候事务就会被回滚,但是如果异常被捕获,就会被认为是正常执行,依然会提交事务.
修改上述代码:
@RestController
@RequestMapping("/user2")
public class TransactionController2 {
@Autowired
private UserService userService;
@RequestMapping("/registry2")
@Transactional
public String registry2(String userName,String password){
userService.registryUser(userName,password);
try {
int i = 10/0;
}catch (ArithmeticException e) {
e.printStackTrace();
}
return "注册成功";
}
}
运行程序之后,虽然出错了,由于异常得到了捕获,事务便得到了提交.
以下两种情况,事务依然会回滚:
- 重新抛出异常
try {
int i = 10/0;
}catch (ArithmeticException e) {
throw e;
}
- 手动回滚事务
使用TransactionAspectSupport.currentTransactionStatus()
得到当前事务,并使用setRollbackOnly
回滚.
try {
int i = 10/0;
}catch (ArithmeticException e) {
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
3. @Transactional详解
我们主要学习@Transactional
注解的三个属性:
1. rollbackFor:异常回滚属性,指定能够触发事务回滚的异常类型.可以指定多个异常类型.
2. Isolation:事务的隔离级别,默认是Isolation.DEFAULT.
3. propagation:事务的传播机制.默认值为propagation.REQUIRED
3.1 rollbackFor
异常回滚的时候,@Transactional
默认在遇到Error或者运行时异常时才会回滚.
接下来我们来使用代码验证:
@Transactional
@RequestMapping("/registry3")
public String registry3(String userName,String password) throws IOException {
userService.registryUser(userName,password);
if (true){
throw new IOException();
}
return "注册成功";
}
向服务器提交数据:
我们看到,虽然抛出了异常,但是数据库的数据仍然被修改了.
如果我们要想指定回滚异常的类型,我们需要通过@Transactional
的rollbackFor属性来完成,给属性传入异常的类对象来实现对回滚异常的指定,
@RequestMapping("/registry3")
@Transactional(rollbackFor = Exception.class)
public String registry3(String userName,String password) throws IOException {
userService.registryUser(userName,password);
if (true){
throw new IOException();
}
return "注册成功";
}
运行程序:
我们看到,事务并没有进行提交,被回滚了,数据库的数据并没有更行.
3.2 事务隔离级别
3.2.1 回顾MySQL中的事务隔离级别
https://lilesily12385.blog.csdn.net/article/details/137935719
3.3.2 Spring事务隔离级别
Spring中事务的隔离级别有5种:
- Isolation.DEFAULT: 以连接数据库的隔离级别为准.
- Isolation.READ_UNCOMMITTED:读未提交
- Isolating.READ_COMMITTED:读提交.
- Isolation.REPEATABLE_READ:可重复读.
- Isolation.SERIALIZABLE:串行化.
public enum Isolation {
DEFAULT(-1),
READ_UNCOMMITTED(1),
READ_COMMITTED(2),
REPEATABLE_READ(4),
SERIALIZABLE(8);
private final int value;
private Isolation(int value) {
this.value = value;
}
public int value() {
return this.value;
}
}
Spring中的隔离级别可以通过@Transactional
中的Isolation
属性进行设置.
@RequestMapping("/registry3")
@Transactional(isolation = Isolation.DEFAULT)
public String registry3(String userName,String password) throws IOException {
userService.registryUser(userName,password);
return "注册成功";
}
3.3 Spring事务传播机制
3.3.1 概念
事务的传播机制就是:多个事务方法存在调用关系的时候,事务是如何在这些方法之间进行传播的.事务的传播机制就解决的是一个事务在多个节点上(方法)中的传递.
比如有两个方法A和B,他们都被@Transactional
修饰,方法A调用了方法B.A方法运行的时候,会开启一个新的事物,当A调用B的时候,B方法本身也有事务,此时B方法运行的时候,是假如A事务,还是创建一个新的事务呢?下面我们就来介绍一下事务的传播机制.
3.3.2 事务的传播机制分类
@Transactional
注解支持事务的传播机制的设置,我们可以通过propagation
属性来设置传播行为.
- Propagation.REQUIRED:加入事务,默认的事务传播级别.如果当前存在事务,则加入该事务,如果当前没有事务,则创建一个新的事务.(加入事务,就是共用一个事务,一个事务发生异常,全部回滚.)
- Propagation.SUPPORTS:如果当前存在事务,则加入该事务.如果当前没有事务,则以非事务的方式继续运行.
- Propagation.MANDATORY:强制性,如果当前存在事务,则加入该事务,如果当前没有事务,则抛出异常.
- Propagation.REQUIERS_NEW:新建事务,创建一个新事务如果当前存在事务,则把当前事务挂起.(发生异常,不影响其他事务)也就是不管外部方法是否开启事务,
Propagation.REQUIERS_NEW
修饰的内部方法都会新开启自己的事务,且开启的事务相互独立,互不干涉. - Propagation.NOT_SUPPORTED: 以非事务的方式运行,如果当前存在事务则把当前事务挂起.
- Propagation.NEVER: 不支持当前事务,以非事务的方式运行,如果存在事务,则抛出异常.
- Propagation.NESTED: 嵌套事务,如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行,如果当前没有事务,则该取值等价于
Propagation.REQUIRED
.
1,4对应,2,5对应,3,6对应.
public enum Propagation {
REQUIRED(0),
SUPPORTS(1),
MANDATORY(2),
REQUIRES_NEW(3),
NOT_SUPPORTED(4),
NEVER(5),
NESTED(6);
private final int value;
private Propagation(int value) {
this.value = value;
}
public int value() {
return this.value;
}
}
举例说明:一对新人结婚,需要房子
- Propagation.REQUIRES: 如果你有房子,就住你的房子(加入事务),如果你没有房子,我们就一起买房子(创建一个新事务).
- Propagation.SUPPORTS: 如果你有房子,我们就住你的房子(加入事务),如果没有房子,我们就租房子(以非事务的方式运行).
- Propagation.MANDATORY: 要求必须有房子(加入事务),如果没有房子,就不结婚(抛出异常)
- Propagation.REQUIERS_NEW: 必须买新房,不管你有没有房子,必须两个人一起买房(创建一个新事务),即使有房也不住(当前事务挂起).
- Propagation.NOT_SUPPORTED: 不管你有没有房子,我都不住(挂起当前事务),必须租房(以非事务的方式运行).
- Propagation.NEVER:不能有房子(当前存在事务),有房子就不结婚(抛出异常).
- Propagation.NESTED :如果你没房,就⼀起买房.如果你有房,我们就以房子为根据地,做点下生意.(如果如果当前存在事务,则创建⼀个事务作为当前事务的嵌套事务来运行.如果当前没有事务,则该取值等价于PROPAGATION_REQUIRED )
3.3.3 Spring事务传播机制代码演示
重点关注两个: REQUIRED,REQUIERS_NEW.
- REQUIRED(加入事务)
用户注册,插入一条数据,并记录操作日志.
@RequestMapping("/r4")
@Transactional
public String registry4(String userName,String password){
userService.registryUser(userName,password);
logService.insertLog(userName,"用户注册");
return "注册成功";
}
@Service
public class UserService {
@Autowired
public UserInfoMapper userInfoMapper;
@Transactional(propagation = Propagation.REQUIRED)
public void registryUser(String name,String password) {
userInfoMapper.insert(name, password);
}
}
@Service
public class LogService {
@Autowired
private LogInfoMapper logInfoMapper;
@Transactional(propagation = Propagation.REQUIRED)
public void insertLog(String name,String op){
int i = 10/0;
//记录⽤⼾操作
logInfoMapper.insertLog(name,"⽤户注册");
}
}
我们在执行之后,发现数据库中并没有插入任何数据,这就是因为insertLog
方法发生了异常,事务发生回滚,当事务回滚之后,registry4
方法也发生了回滚,导致了registryUser
也发生了回滚,导致数据库中没有插入数据.
- REQUIRES_NEW(新建事务)
将上面的UserService和LogService的事务传播机制改为Propagation.REQUIRES_NEW.
@RequestMapping("/r4")
@Transactional
public String registry4(String userName,String password){
userService.registryUser(userName,password);
logService.insertLog(userName,"用户注册");
return "注册成功";
}
@Service
public class LogService {
@Autowired
private LogInfoMapper logInfoMapper;
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void insertLog(String name,String op){
int i = 10/0;
//记录⽤⼾操作
logInfoMapper.insertLog(name,"⽤户注册");
}
}
@Service
public class UserService {
@Autowired
public UserInfoMapper userInfoMapper;
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void registryUser(String name,String password) {
userInfoMapper.insert(name, password);
}
}
在执行之后,我们会发现,日志表中并没有插入新的数据,但是用户表中插入了新的数据,这是由于UserService
,LogService
与registry4
属于不同的事务,LogService
出现异常回滚之后不会影响registry4
和UserService
的执行.
- NEVER(不支持当前事务)
把REQUIRED
代码的UserService
中的对应方法的事务传播机制修改为Propagation.NEVER
.并去掉制造的异常.
@Transactional(propagation = Propagation.NEVER)
public void insertLog(String name,String op){
//记录⽤⼾操作
logInfoMapper.insertLog(name,"用户注册");
}
运行之后,程序抛出异常,日志表和用户表均没有数据插入.
- NESTED(嵌套事务)
将上述REQUIRED
的UserService
中的对应方法的事务传播机制修改为Propagation.NESTED
.
@Transactional(propagation = Propagation.NESTED)
public void insertLog(String name,String op){
int i = 10/0;
//记录用户操作
logInfoMapper.insertLog(name,"用户注册");
}
@Transactional(propagation = Propagation.NESTED)`在这里插入代码片`
public void registryUser(String name,String password) {
userInfoMapper.insert(name, password);
}
运行程序之后,两张表中都没有插入任何数据,如果我们对出现异常的方法进行手动回滚:
@Transactional(propagation = Propagation.NESTED)
public void insertLog(String name,String op){
try {
int i = 10/0;
}catch (Exception e){
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
//记录用户操作
logInfoMapper.insertLog(name,"用户注册");
}
我们看到,用户表插入成功了,但是日志表的数据被回滚了.
区分REQUIRED和NESTED
REQUIRED:当其中一个事务出现异常的时候,所有事务都会回滚,如果try-catch语句中对事物进行手动回滚,则子事务和父事务全部会被回滚.
NESTED : 当子事务出现异常的时候,子事务对应的父事务也会回滚,但是如果在有异常的子事务中进行try-catch,catch中对事务进行手动回滚,则只有出现异常的事务被回滚,但是另一个没有出现异常的子事务没有被回滚,与REQUIRED最大的区别就是,NESTED可以做到部分回滚,但是REQUIRED只能做到全部回滚.