目录
什么是事务?
事务:指作为单个逻辑工作单元执行的一系列操作,是一组操作的集合,是一个不可分割的操作。在数据库管理系统中,事务通常用于确保数据库操作的原子性、一致性、隔离性和持久性。
事务会把所有的操作作为一个整体,一起向数据库提交或是撤销操作的请求,这组操作要么同时成功,要么同时失败
为什么需要事务?
事务的存在是为了保证数据库操作的一致性、完整性和可靠性
例如:
在进行转账操作时(A账户向B账户转账):
第一步:A 账户 -1000 元
第二步:B账户 +1000 元
若没有事务,第一步执行成功,但第二步执行失败了,此时 A 账户中的 1000 元就平白无故消失了。此时使用事务,让 A-1000 B+1000 作为一组操作,这组操作要么一起成功,要么一起失败
事务的操作
事务的操作主要有三步:
1. 开启事务 start transaction/ begin(一组操作前开启事务)
2. 提交事务 commit(这组操作全部成功,提交事务)
3. 回滚事务 rollback(这组操作中间任意一个操作出现异常,回滚事务)
Spring中事务的实现
Spring 中的事务操作分为两类:
1. 编程式事务(手动写代码操作事务)
2. 声明式事务(利用注解自动开启和提交事务)
接下来,我们通过用户注册的例子来进一步理解Spring中事务的实现
数据准备
创建数据表
要实现用户注册,我们首先需要准备两张表:user_info(用户表)、log_info(操作日志表)
-- 创建数据库
DROP DATABASE IF EXISTS trans_test;
CREATE DATABASE trans_test DEFAULT CHARACTER SET utf8mb4;
use trans_test;
-- 创建用户表
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';
接下来,我们引入依赖、创建项目、连接数据库...
创建实体类
model
UserInfo:
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;
}
LogInfo:
import 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;
}
在进行用户注册时,我们需要向用户表user_info 和 日志表log_info 中插入数据:
mapper
UserMapper:
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper {
@Insert("insert into user_info (user_name, password) values (#{userName}, #{password})")
Integer insertUser(String userName, String password);
}
LogMapper:
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface LogMapper {
@Insert("insert into log_info (user_name, op) values (#{userName}, #{op})")
Integer insertLog(String userName, String op);
}
service
UserService:
import com.example.springtrans.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public Integer insertUser(String userName, String password) {
return userMapper.insertUser(userName, password);
}
}
LogService:
import com.example.springtrans.mapper.LogMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class LogService {
@Autowired
private LogMapper logMapper;
public Integer insertLog(String userName, String op){
return logMapper.insertLog(userName, op);
}
}
controller
userController:
import com.example.springtrans.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/registry")
public Boolean registry(String userName, String password){
//注册
Integer result = userService.insertUser(userName, password);
return true;
}
}
Spring 编程式事务
Spring手动操作事务有3个重要操作步骤:
开启事务
提交事务
回滚事务
import com.example.springtrans.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.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/user")
public class UserController {
//JDBC 事务管理器
@Autowired
private DataSourceTransactionManager dataSourceTransactionManager;
// 定义事务属性
@Autowired
private TransactionDefinition transactionDefinition;
@Autowired
private UserService userService;
@RequestMapping("/registry")
public Boolean registry(String userName, String password){
//开启事务
TransactionStatus transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition);
//注册
Integer result = userService.insertUser(userName, password);
//提交事务
dataSourceTransactionManager.commit(transactionStatus);
return true;
}
}
SpringBoot 内置了两个对象:
DataSourceTransactionManager :事务管理器,用来开启、提交或回滚事务
TransactionDefinition:事务的属性,在开启事务的时候需要将 TransactonDefinition 传递进取从而获取一个事务 TransactionStatus
我们运行程序,观察事务的提交:
http://127.0.0.1:8080/user/registry?userName=zhangsan&password=zhangsan
观察数据库,数据插入成功:
接下来,我们观察事务回滚:
import com.example.springtrans.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.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/user")
public class UserController {
//JDBC 事务管理器
@Autowired
private DataSourceTransactionManager dataSourceTransactionManager;
// 定义事务属性
@Autowired
private TransactionDefinition transactionDefinition;
@Autowired
private UserService userService;
@RequestMapping("/registry")
public Boolean registry(String userName, String password){
//开启事务
TransactionStatus transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition);
//参数校验
if(!StringUtils.hasLength(userName) || !StringUtils.hasLength(password)){
return false;
}
//注册
Integer result = userService.insertUser(userName, password);
//回滚事务
dataSourceTransactionManager.rollback(transactionStatus);
// //提交事务
// dataSourceTransactionManager.commit(transactionStatus);
return true;
}
}
再次运行,访问:
http://127.0.0.1:8080/user/registry?userName=zhangsan&password=zhangsan
此时再观察数据库:
数据库中并未新增数据
我们对比两次打印的日志:
从日志可以看到两次都执行了插入操作,且插入成功,但由于第二次事务进行了回滚,因此数据库中没有新增数据
Spring 声明式事务
使用声明式事务只需要在需要事务的方法上添加 @Transactional 注解就可以实现了,无需手动开启事务和提交事务,在进入方法时会自动开启事务,而当方法执行完会自动提交事务,若中途发生了未处理的异常,就会自动回滚事务
@Transactional
@RequestMapping("/registry")
public Boolean registry(String userName, String password){
//参数校验
if(!StringUtils.hasLength(userName) || !StringUtils.hasLength(password)){
return false;
}
//注册
Integer result = userService.insertUser(userName, password);
return true;
}
再次运行程序,观察数据库:
从自增 id 值我们也可以看出:在之前进行回滚事务时,数据插入到用户表中,但由于事务回滚,因此数据库没有新增数据
若在执行过程中出现异常:
@Transactional
@RequestMapping("/registry")
public Boolean registry(String userName, String password){
//参数校验
if(!StringUtils.hasLength(userName) || !StringUtils.hasLength(password)){
return false;
}
//注册
Integer result = userService.insertUser(userName, password);
int a = 1 / 0;
return true;
}
重新运行程序,观察数据库和打印的日志:
数据插入成功,但数据库没有新增数据,事务进行了回滚
事务的控制一般是在业务逻辑层中的,这是因为在业务逻辑层当中,一个业务功能可能会包含多个数据访问的操作,在业务逻辑层来控制事务,就可以将多个数据访问操作控制在一个事务范围内
在这里,为了方便最开始的学习,因此我们将其写在 controller
@Transactional
@Transactional 可以用来修饰方法或类:
修饰方法:只有修饰 public 方法时才会生效(修饰其他方法时不会报错,但也不会生效)
修饰类:对 @Transactional 修饰的类中所有的 public 方法生效
当 类/方法被 @Transactional 修饰时,在目标方法执行开始前,会自动开启事务,方法执行结束后,会自动提交事务
若在方法执行的过程中,出现了异常,且异常未被捕获,就会进行事务回滚
若异常被程序捕获,方法就会被认为是成功执行,仍然会提交事务
我们对上述异常进行捕获:
@Transactional
@RequestMapping("/registry")
public Boolean registry(String userName, String password){
//参数校验
if(!StringUtils.hasLength(userName) || !StringUtils.hasLength(password)){
return false;
}
//注册
Integer result = userService.insertUser(userName, password);
try {
int a = 1 / 0;
}catch (Exception e){
e.printStackTrace();
}
return true;
}
重新运行程序,观察数据库,数据插入成功:
此时虽然程序出错了,但由于异常被捕获了,所以事务依然得到了提交
若需要事务进行回滚,有两种方式:
1. 重新抛出异常
@Transactional
@RequestMapping("/registry")
public Boolean registry(String userName, String password){
//参数校验
if(!StringUtils.hasLength(userName) || !StringUtils.hasLength(password)){
return false;
}
//注册
Integer result = userService.insertUser(userName, password);
try {
int a = 1 / 0;
}catch (Exception e){
throw e;
}
return true;
}
此时重新运行事务,观察数据库:
此时在运行过程中出现了异常,且异常被捕获,但由于异常又被抛出,因此事务进行了回滚
2. 手动回滚事务
@Transactional
@RequestMapping("/registry")
public Boolean registry(String userName, String password){
//参数校验
if(!StringUtils.hasLength(userName) || !StringUtils.hasLength(password)){
return false;
}
//注册
Integer result = userService.insertUser(userName, password);
try {
int a = 1 / 0;
}catch (Exception e){
//手动回滚事务
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
return true;
}
再次运行:
在运行过程中出现了异常,且异常被捕获,但由于设置了事务回滚,因此事务进行了回滚
接下来,我们来了解 @Transactional 注解中的三个常见属性:
1. rollbackFor:异常回滚属性,指定能够触发事务回滚的异常类型,可以指定多个异常类型
2. Isolaction:事务的隔离级别 默认值为 Isolaction.DEFAULT
3. propagation:事务的传播机制 默认值为 Propagation.REQUIRED
rollbackFor
我们来看一个例子:
@Transactional
@RequestMapping("/registry")
public Boolean registry(String userName, String password) throws IOException {
//参数校验
if(!StringUtils.hasLength(userName) || !StringUtils.hasLength(password)){
return false;
}
//注册
Integer result = userService.insertUser(userName, password);
log.info("数据插入, {}", result);
// 数据插入成功,抛出异常
if(result > 0){
throw new IOException();
}
return true;
}
此时运行代码,观察打印的日志:
此时虽然抛出了异常,但是事务依然进行了提交
此时虽然发生了未处理的异常,但事务依然进行了提交,未进行回滚,这是为什么呢?
这是因为 @Transactional 默认只有在遇到 运行时异常 和 ERROR 时才会回滚,非运行时异常时不会回滚,即 Exception的子类中,除了 RuntimeException及其子类
若需要回滚指定类型的异常,可以通过 rollbackFor 属性来指定
@Transactional(rollbackFor = Exception.class)
@RequestMapping("/registry")
public Boolean registry(String userName, String password) throws IOException {
//参数校验
if(!StringUtils.hasLength(userName) || !StringUtils.hasLength(password)){
return false;
}
//注册
Integer result = userService.insertUser(userName, password);
log.info("数据插入, {}", result);
// 数据插入成功,抛出异常
if(result > 0){
throw new IOException();
}
return true;
}
此时再运行程序:
此时事务进行了回滚
事务隔离级别
事务的隔离级别解决的是多个事务同时调用一个数据库的问题
我们首先来回顾MySQL的事务隔离级别
SQL标准定义了四种隔离级别,MySQL全都支持,这四种隔离级别分别是:
(1)读未提交(Read Uncommitted):事务中的修改可以被其他事务读取,即一个事务可以读到另一个事务未提交的更新数据。此级别会产生脏读、不可重复读、幻读等问题。
其他事务未提交的数据可能会发生回滚,但该隔离级别可以读到,该级别读到的数据称为脏数据,这个现象叫做脏读
(2)读已提交(Read Committed):一个事务只能读到另一个事务已经提交的数据,避免了脏读,但是可能会出现不可重复读和幻读
在事务执行中可以读到其他事务提交的结果,在不同的时间,相同的SQL查询结果可能不同,这种现象叫做不可重复读
(3)可重复读(Repeatable Read):保证了在同一事务中多次读取同一数据时,结果始终相同,避免了不可重复读。但是仍然可能出现幻读。
当此级别的事务正在执行时,另一个事务成功插入了某条数据,但因为它每次查询的结果是相同的,此时就会导致查询不到这条数据,自己重复插入时失败(唯一约束的原因),在事务中查询不到这条信息,但自己就是插入不进去,这个现象叫做幻读
(4)序列化(Serializable):最高的隔离级别,强制事务串行执行,避免了脏读、不可重复读、幻读等问题。但是也带来了性能上的损失。
隔离级别越高,数据库的并发性能就越差。因此,在选择隔离级别时,需要根据实际情况进行权衡。
事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交(Read Uncommitted) | √ | √ | √ |
读已提交(Read Committed) | × | √ | √ |
可重复读(Repeatable Read) | × | × | √ |
序列化(Serializable) | × | × | × |
Spring 事务隔离级别:
Spring 中事务隔离级别有5种:
Isolation.DEFAULT:以连接的数据库的事务隔离级别为主
Isolation.READ_UNCOMMITTED:读未提交,对应SQL标准中的 Read Uncommitted
Isolation.READ_COMMITTED:读已提交,对应SQL标准中的 Read Committed
Isolation.REPEATABLE_READ:可重复读,对应SQL标准中的 Repeatable Read
Isolation.SERIALIZABLE:串行化,对应SQL标准中的 Serializable
Spring 中事务隔离级别可以通过 @Transactional 中的 isolation 属性进行设置
@Transactional( isolation = Isolation.READ_COMMITTED)
事务传播机制
什么是事务传播机制?
事务传播机制:多个事务方法存在调用关系时,事务是如何在这些方法之间进行传播的
例如:
存在方法A和B都被 @Transactional 修饰,A方法调用B方法
A方法运行时,会开启一个事务,而当A调用B时,B方法本身也有事务,此时B方法运行时,是加入A的事务,还是创建一个新的事务呢?
这就涉及到事务的传播机制了,事务的传播机制解决的是一个事务在多个节点(方法)中传递的问题
事务的传播机制有:
Spring 事务传播机制有以下7种:
Propagation.REQUIRED:默认的事务传播级别,若当前存在事务,则加入该事务;若当前没有事务,则创建一个新的事务
若 A 存在事务,调用B方法时,B方法使用 A的事务;若A不存在事务,则B方法创建一个新的事务
Propagation.SUPPORTS:若当前存在事务,则加入该事务;若当前没有事务,则以非事务的方式继续运行
若 A 存在事务,调用B方法时,B方法使用 A的事务;若A不存在事务,则B方法以非事务的方式运行
Propagation.REQUIRES_NEW:无论当前是否存在事务,都会创建一个新的事务,且开启的事务互相独立,互不干扰
若 A 存在事务,调用B方法时,B方法将A的事务挂起(不用),且开启新的事务;若A不存在事务,B方法开启新的事务
Propagation.NOT_SUPPORTED:以非事务的方式运行,若当前存在事务,则将当前事务挂起
若 A 存在事务,调用B方法时,B方法将A的事务挂起(不用),以非事务的方式运行;若A不存在事务,B方法以非事务的方式运行
Propagation.MANDATORY:若当前存在事务,则加入该事务,若当前没有事务,则抛出异常
若 A 存在事务,调用B方法时,B方法使用 A的事务;若A不存在事务,则抛出异常
Propagation.NEVER:以非事务的方式运行,若当前存在事务,则抛出异常
若 A 存在事务,调用B方法时,则抛出异常;若A不存在事务,B以非事务的方式运行
Propagation.NESTED:若当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行,若当前没有事务,则创建一个新的事务
若 A 存在事务,调用B方法时,B方法创建一个事务作为当前事务的嵌套事务来运行;若A不存在事务,则B方法创建一个新的事务
@Transactional 注解支持事务传播的设置,通过 propagation 属性来指定传播行为
我们还是通过用户注册来进一步理解Spring的事务传播机制
当进行用户注册时,需要向用户表插入用户数据,向操作日志表插入用户操作记录
我们首先来看 REQUIRED:
UserController:
public class UserController {
@Autowired
private UserService userService;
@Autowired
private LogService logService;
@Transactional( propagation = Propagation.REQUIRED)
@RequestMapping("/registry")
public Boolean registry(String userName, String password) throws IOException {
//参数校验
if(!StringUtils.hasLength(userName) || !StringUtils.hasLength(password)){
return false;
}
//注册
Integer userResult = userService.insertUser(userName, password);
log.info("数据插入 user_info:{}", userResult);
Integer logResult = logService.insertLog(userName, "用户注册");
log.info("数据插入 log_info: {}", logResult);
return true;
}
}
UserService:
public class UserService {
@Autowired
private UserMapper userMapper;
@Transactional(propagation = Propagation.REQUIRED)
public Integer insertUser(String userName, String password) {
return userMapper.insertUser(userName, password);
}
}
LogService:
public class LogService {
@Autowired
private LogMapper logMapper;
@Transactional(propagation = Propagation.REQUIRED)
public Integer insertLog(String userName, String op){
int result = logMapper.insertLog(userName, op);
int a = 1 / 0;
return result;
}
}
运行程序,观察数据库,此时没有插入任何数据
(1)registry 方法开启事务
(2)接着调用 userService.insertUser 方法,向user_info 插入一条数据(执行成功),(与registry 使用同一个事务)
(3)然后registry调用 logService.insertLog 方法,向 log_info 插入一条数据(插入成功),但由于出现了异常,因此执行失败(与registry 使用同一个事务)
(4)由于(3)出现了异常,因此事务回滚,(2)与(3)使用的同一个事务,因此(2)的数据也回滚了
REQUIRES_NEW
将上述userService.insertUser 和 logService.insertLog 的事务传播机制改为 Propagation.REQUIRES_NEW
运行程序,此时用户表数据插入成功了,日志表数据插入失败
(1)registry方法开启事务
(2)接着调用 userService.insertUser 方法,向user_info 插入一条数据(执行成功),(使用新开启的事务)
(3)然后registry调用 logService.insertLog 方法,向 log_info 插入一条数据(插入成功),但由于出现了异常,因此执行失败(使用新开启的事务)
(4)由于(3)出现了异常,因此事务回滚,(2)与(3)使用的是不同事务,互不干涉,因此(2)的数据不会进行回滚
其他的事务传播机制在这里就不再进行分析了,大家可以自行测试和分析
接下来,我们来看 NESTED (嵌套事务)
将userService.insertUser 和 logService.insertLog 的事务传播机制改为 Propagation.NESTED
再次运行程序,发现没有数据插入:
(1)registry方法开启事务
(2)接着调用 userService.insertUser 方法,向user_info 插入一条数据(执行成功),(嵌套registry的事务)
(3)然后registry调用 logService.insertLog 方法,向 log_info 插入一条数据(插入成功),但由于出现了异常,因此执行失败(嵌套registry的事务)
(4)由于(3)出现了异常,因此事务回滚,使用的是嵌套事务,因此 logService.insertLog往上找调用它的方法和事务,因此 userService.insertUser 也失败了,所以(2)的数据也回滚了
registry 事务可以认为是父事务,嵌套事务是子事务,父事务出现异常,子事务也会回滚,子事务出现异常如果不进行处理,也会导致父事务回滚
当事务执行成功时,NESTED 和 REQUIRED 的结果是相同的,而上述执行失败时,此时结果也与 REQUIRED 结果相同,那么 NESTED 与 REQUIRED 有什么不同呢?
NESTED 和 REQUIRED 的区别
我们修改 LogService
public class LogService {
@Autowired
private LogMapper logMapper;
@Transactional(propagation = Propagation.NESTED)
public Integer insertLog(String userName, String op){
int result = logMapper.insertLog(userName, op);
try {
int a = 1 / 0;
}catch (Exception e){
// 回滚当前事务
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
return result;
}
}
运行代码,此时用户表数据插入成功了,日志表数据插入失败
而当我们将 logService.insertLog 事务的传播机制修改为 REQUIRED:
此时再次运行程序,用户表和日志表的数据都添加失败
我们可以看到:
当事务全部执行成功时,NESTED 和 REQUIRED 的结果是相同的
当事务部分执行成功时,REQUIRED 会导致整个事务全部回滚,而 NESTED 嵌套事务可以实现局部回滚(不影响上一个方法中的执行结果)
嵌套事务之所以能够实现部分事务的回滚,是因为事务中有一个保存点(savepoint),嵌套事务进入后相当于新建了一个保存点,而回滚时只是回滚到当前保存点
REQUIRED 是加入到当前事务中的,并没有创建事务的保存点,因此出现了回滚就是整个事务回滚