目录✅
- 1. 回顾为什么需要事务?
- 2. Spring 中事务的实现
- 2.1 回顾MySQL中事务的使用
- 2.2 Spring 手动操作事务
- 2.3 Spring 声明式事务(自动事务)
- 2.4 @Transactional 作用范围
- 2.5 @Transactional 参数说明
- 2.6 @Transactional 工作原理
- 3. 事务隔离级别
- 3.1 mysql 事务特性回顾
- 3.2 MySQL 4种事务隔离级别
- 3.3 Spring 中设置事务隔离级别
- 3.4 Spring 事务的5种隔离级别
1. 回顾为什么需要事务?
- 事务定义:将⼀组操作封装成⼀个执⾏单元(封装到⼀起),要么全部成功,要么全部失败
- 事务应用场景:转账(最典型),第一步操作:A账户-100元,第二步操作:B账户+100元。如果没有事务,那么当第一步执行成功了,第二步执行失败(比如服务器掉电),那么A账户平白无故的100元就“人间蒸发”了。这是一个很严重的错误;而如果使用事务就可以解决这个问题,让这一组操作要么一起成功,要么一起失败。
2. Spring 中事务的实现
Spring 中的事务操作分为两类:
- ⼿动操作事务。(手动挡:麻烦)
- 声明式⾃动提交事务。(自动挡:简单)
2.1 回顾MySQL中事务的使用
事务在 MySQL 有 3 个重要的操作:开启事务、提交事务、回滚事务,它们对应的操作命令如下:
-- 开启事务
start transaction;
-- 业务执⾏
-- 提交事务
commit;
-- 回滚事务
rollback;
2.2 Spring 手动操作事务
Spring ⼿动操作事务和上⾯ MySQL 操作事务类似,它也是有 3 个重要操作步骤:
- 开启事务(获取事务)。
- 提交事务。
- 回滚事务。
SpringBoot 内置了两个对象,DataSourceTransactionManager
⽤来获取事务(开启事务)、提交或回滚事务的,⽽ TransactionDefinition
是事务的属性,在获取事务的时候需要将TransactionDefinition
传递进去从⽽获得⼀个事务 TransactionStatus
,实现代码如下:
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@Autowired // JDBC 事务管理器
private DataSourceTransactionManager dataSourceTransactionManager;
@Autowired // 定义事务属性
private TransactionDefinition transactionDefinition;
// 此方法中使用编程式事务 (手动挡)
@RequestMapping("/add")
public int add(UserInfo userInfo){
// 非空校验
if (userInfo==null || !StringUtils.hasLength(userInfo.getUsername())||!StringUtils.hasLength(userInfo.getPassword())){
return 0;
}
// 获取事务(开启事务)
TransactionStatus transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition);
// 插入数据库
int result=userService.add(userInfo);
System.out.println("受影响的行数:"+result);
// 提交事务
dataSourceTransactionManager.commit(transactionStatus);
// 回滚事务
//dataSourceTransactionManager.rollback(transactionStatus);
return result;
}
}
此时执行代码向数据库中插入记录,设置了commit就能成功插入,设置rollback就插入失败;
从上述代码可以看出,以上代码虽然可以实现事务,但操作很繁琐,这时候就可以使用声明式事务(自动挡)来简化代码:
2.3 Spring 声明式事务(自动事务)
声明式事务的实现很简单,只需要在需要的⽅法(或类)上添加 @Transactional
注解就可以实现了,⽆需⼿动开启事务和提交事务,进⼊⽅法时⾃动开启事务,⽅法执⾏完会⾃动提交事务,如果中途发⽣了没有处理的异常会⾃动回滚事务,具体实现代码如下:
// 此方法中使用声明式事务 (自动挡)
// 在进入方法之前自动开启事务,在方法执行完之后,自动提交事务,如果出现异常,则自动回滚事务
@Transactional
@RequestMapping("/add2")
public int add2(UserInfo userInfo){
// 非空校验
if (userInfo==null || !StringUtils.hasLength(userInfo.getUsername())||!StringUtils.hasLength(userInfo.getPassword())){
return 0;
}
int result=userService.add(userInfo);
System.out.println("受影响的行数:"+result);
// int n=10/0;
return result;
}
测试:
给数据库中插入用户(用户名+密码):
此时数据库中只有一条记录:
此时访问对应接口,并给接口中传入username和password:
插入成功执行:
但如果代码中出现触发异常的语句:(被除数为0会触发算术异常)
此时再访问接口:(就会报500的错误,事务就会回滚,插入数据就不成功了)
可以看到插入一开始是成功了,返回受影响的行数为1,但是此时查看数据库,发现没有这条插入的记录(事务回滚了):
这时如果手动的处理(try catch)这个异常,代码就会认为这个异常是可控的,就不会触发事务的回滚操作:
访问接口:
可以看到虽然代码中出现了异常,但是事务不会回滚,数据插入成功:
这与我们的理念(代码出现异常事务就得回滚)不符合,解决这个问题的方式有如下2种方法:
@Transactional
@RequestMapping("/add3")
public int add3(UserInfo userInfo){
// 非空校验
if (userInfo==null || !StringUtils.hasLength(userInfo.getUsername())||!StringUtils.hasLength(userInfo.getPassword())){
return 0;
}
int result=userService.add(userInfo);
System.out.println("受影响的行数:"+result);
try {
int n=10/0;
}catch (Exception e){
// 解决方案1:异常
// 抛出去
// throw e;
//解决方案2:
// 手动回滚
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
return result;
}
此时设置了手动回滚后,出现异常的代码即使被try catch处理也会触发事务的回滚:
虽然返回受影响的行数为1,插入成功,但是数据库中并没有这条记录:
事务回滚(rollback)成功~
2.4 @Transactional 作用范围
@Transactional 可以⽤来修饰⽅法或类:
- 修饰⽅法时:需要注意只能应⽤到 public ⽅法上,否则不⽣效。推荐此种⽤法。
- 修饰类时:表明该注解对该类中所有的 public ⽅法都⽣效。
2.5 @Transactional 参数说明
@Transactional 可以设置的参数如下图:
具体使用说明讲解:
2.6 @Transactional 工作原理
@Transactional 是基于 AOP 实现的,AOP ⼜是使⽤动态代理实现的。如果⽬标对象实现了接⼝,默认
情况下会采⽤ JDK 的动态代理,如果⽬标对象没有实现了接⼝,会使⽤ CGLIB 动态代理。
@Transactional 在开始执⾏业务之前,通过代理先开启事务,在执⾏成功之后再提交事务。如果中途遇
到的异常,则回滚事务。
@Transactional 实现思路预览:
@Transactional 具体执⾏细节如下图所示:
3. 事务隔离级别
3.1 mysql 事务特性回顾
事务有4 大特性(ACID),原⼦性、持久性、⼀致性和隔离性,具体概念如下:
- 原⼦性:⼀个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中
间某个环节。事务在执⾏过程中发⽣错误,会被回滚(Rollback)到事务开始前的状态,就像这个
事务从来没有执⾏过⼀样。 - ⼀致性:在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写⼊的资料必须完
全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以⾃发性地完成预定的⼯
作。 - 持久性:事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。(写入硬盘)
- 隔离性:数据库允许多个并发事务同时对其数据进⾏读写和修改的能⼒,隔离性可以防⽌多个事务
并发执⾏时由于交叉执⾏⽽导致数据的不⼀致。事务隔离分为不同级别,包括 读未提交(Read
uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行(Serializable)。
上⾯ 4 个属性,可以简称为ACID。
原⼦性(Atomicity,或称不可分割性)
⼀致性(Consistency)
隔离性(Isolation,⼜称独⽴性)
持久性(Durability)
⽽这 4 种特性中,只有隔离性(隔离级别)是可以设置的(设置事务的隔离级别是⽤来保障多个并发事务执⾏更可控,更符合操作者预期的;事务的隔离级别是为了防⽌其他的事务影响当前事务执⾏的⼀种策略)
3.2 MySQL 4种事务隔离级别
- READ UNCOMMITTED:读未提交,也叫未提交读,该隔离级别的事务可以看到其他事务中未提交
的数据。该隔离级别因为可以读取到其他事务中未提交的数据,⽽未提交的数据可能会发⽣回滚,因此我们把该级别读取到的数据称之为脏数据,把这个问题称之为脏读。 - READ COMMITTED:读已提交,也叫提交读,该隔离级别的事务能读取到已经提交事务的数据,
因此它不会有脏读问题。但由于在事务的执⾏中可以读取到其他事务提交的结果,所以在不同时间
的相同 SQL 查询中,可能会得到不同的结果,这种现象叫做不可重复读。 - REPEATABLE READ:可重复读,是 MySQL 的默认事务隔离级别,它能确保同⼀事务多次查询的结果⼀致。但也会有新的问题,⽐如此级别的事务正在执⾏时,另⼀个事务成功的插⼊了某条数
据,但因为它每次查询的结果都是⼀样的,所以会导致查询不到这条数据,⾃⼰重复插⼊时⼜失败
(因为唯⼀约束的原因)。明明在事务中查询不到这条信息,但⾃⼰就是插⼊不进去,这就叫幻读
(Phantom Read)。 - SERIALIZABLE:序列化,事务最⾼隔离级别,它会强制事务排序,使之不会发⽣冲突,从⽽解决
了脏读、不可重复读和幻读问题,但因为执⾏效率低,所以真正使⽤的场景并不多。
- 脏读:⼀个事务读取到了另⼀个事务修改的数据之后,后⼀个事务⼜进⾏了回滚操作,从⽽导致第⼀个事务读取的数据是错误的(脏数据)。
- 不可重复读:⼀个事务两次查询得到的结果不同,因为在两次查询中间,有另⼀个事务把数据修改了。(侧重于修改)
- 幻读:⼀个事务两次查询中得到的结果集不同(就像发生了幻觉一样),因为在两次查询中另⼀个事务有新增了⼀部分数据。(侧重于新增或删除)
补充:在数据库中通过以下 SQL 查询全局事务隔离级别和当前连接的事务隔离级别:
select @@global.tx_isolation,@@tx_isolation;
3.3 Spring 中设置事务隔离级别
Spring 中事务隔离级别可以通过 @Transactional 中的 isolation 属性进⾏设置,具体操作如下图所示:
3.4 Spring 事务的5种隔离级别
Spring 中事务隔离级别包含以下 5 种:
-
Isolation.DEFAULT
:以连接的数据库的事务隔离级别为主。 -
Isolation.READ_UNCOMMITTED
:读未提交,可以读取到未提交的事务,存在脏读。 -
Isolation.READ_COMMITTED
:读已提交,只能读取到已经提交的事务,解决了脏读,存在不可重复读。 -
Isolation.REPEATABLE_READ
:可重复读,解决了不可重复读,但存在幻读(MySQL默认级别)。 -
Isolation.SERIALIZABLE
:串⾏化,可以解决所有并发问题,但性能太低。
从上述介绍可以看出,相⽐于 MySQL 的事务隔离级别,Spring 的事务隔离级别只是多了⼀个Isolation.DEFAULT
(以数据库的全局事务隔离级别为主)
注意事项:
- 当 spring 中设置了事务隔离级别和连接的数据库(mysql)事务隔离级别发生冲突的时候,是以 spring 的为准。
- spring 中的事务隔离级别机制的实现是依靠连接数据库支持事务隔离级别为基础的。
- 本文完结 over ~✨