Spring 与事务

简介

事务是让人生厌的八股,也是面试中的常客。网路上关于 Spring 与事务的问题非常多,然而大部分解答都是在「 背答案」,并没有把它的底层逻辑讲清楚,只要问题一经变通,也就无从下手不知所措。

Spring 使用魔法般的注解 @Transactional 帮我们解决事务的使用问题,给我们带来便利的同时,也屏蔽了底层的细节。屏蔽了底层的细节,也就导致事务相关的使用都是靠着积累的经验,而无法真正理解它。

说明:本文仅对 Spring 中事务的原理进行说明,MySQL 中的事务实现不在本文阐述。

事务是什么

事务这个概念有点抽象,可以把它看做由一堆 SQL 语句组成的操作。

事务可以保证它里面的 SQL 语句要么全部成功,要么全部失败,不存在第三种中间状态。

事务还有 ACID 四种特性,陈词滥调这里不想过多阐述,可以自行 Google 了解。

MySQL 中的事务

首先要说明的是,「 事务」更像是一种约定,数据库可以选择遵守或不遵守该约定。即便是在支持事务的数据库中,它们实现事务的方式也各不相同,MySQL 提供对事务的支持,接下来看看在 MySQL 中要如何使用事务。

在使用事务之前,需要了解事务相关的一些概念。

  • 事务(transaction)指一组 SQL 语句,对应的是整个转账流程。
  • 回滚(rollback)指撤销指定的 SQL 语句
  • 提交(commit)将未存储的 SQL 语句结果写入到数据库
  • 保留点(savepoint)指事务处理中设置的临时占位符,用于事务回滚到指定的 SQL 语句。

事务处理

假设这样一个场景:用户注册一个账号,默认金额是 0 元,之后充值了 100 元,两个操作都在一个事务内。对应的 SQL 语句如下。

1
2
3
4
5
-- MySQL 中标识事务开始
START TRANSACTION;
INSERT INTO user VALUES(1,'zzh',0);
UPDATE zzh SET money = 100 where id = 1;
COMMIT;

上诉操作完成了一个事务的提交,倘若要回滚上诉操作只需要将 COMMIT 替换成 ROLLBACK

1
2
3
4
START TRANSACTION;
INSERT INTO user VALUES(1,'zzh',0);
UPDATE zzh SET money = 100 where id = 1;
ROLLBACK;

不知道你发现了没有,提交和回滚都是针对一组 SQL 进行的。用户注册账号成功,但是充值失败,能否让「 充值失败」不影响到用户注册。

答案是肯定的,上面提到的 SAVEPOINT 就是解决该问题的。

SAVEPOINT 就像游戏存档一样,可以在事务的执行过程中建立多个存档,遇到异常可以随时返回到指定的存档。如下面的语句,「 注册」会成功,而「充值」失败。

1
2
3
4
5
6
7
8
9
10
START TRANSACTION;
INSERT INTO user VALUES(1,'zzh',0);

-- 创建名为 `register_user` 的保留点
SAVEPOINT register_user;
UPDATE zzh SET money = 100 where id = 1;

-- 回滚到 `register_user` 保留点
ROLLBACK TO register_user;
COMMIT;

传统的 JDBC 管理事务

提交与回滚操作

看下这段代码,你是否熟悉。

1
2
3
4
5
6
7
8
9
10
11
12
import java.sql.Connection;

Connection connection = dataSource.getConnection(); // (1)

try (connection) {
connection.setAutoCommit(false); // (2)
// 执行一些 SQL 代码
connection.commit(); // (3)

} catch (SQLException e) {
connection.rollback(); // (4)
}
  1. 获取数据库连接,获取的方式有多种,现在大多数都是维护一个数据库连接池,然后从连接池分配一个连接。
  2. 把获取到的数据库连接,关闭自动提交。因为事务要交由代码管理,而不是让数据库默认提交。
  3. 当执行完 SQL 代码之后,开始提交。
  4. 数据库进行 COMMIT 提交出现异常,代码中进行捕获,并执行回滚操作。

设置隔离级别与保留点(SAVEPOINT)

在  jdbc 中设置数据库隔离级别和 SAVEPOINT 也是非常简单。

1
2
3
4
5
6

connection.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED); // (1)

Savepoint savePoint = connection.setSavepoint(); // (2)

connection.rollback(savePoint); // (3)
  1. setTransactionIsolation api 就可以设置数据库的隔离级别
  2. setSavepoint 创建一个 SAVEPOINT
  3. rollback 到上一个 SAVEPOINT

可以看到 jdbc 中对数据库事务的操作都是非常简单的,Spring 与 jdbc 实现事务的操作并无太大差别,只是他把这些封装的太好,会让你觉得是魔法,难以理解。

Spring 的事务魔法

Transactional 注解

使用 JDBC 开启事务,需要写大量的 try...catch 。通常 try 代码块执行 SQL 操作,catch 中捕获异常进行回滚。

来看下 Spring 中为一个方法添加事务有多简单

1
2
3
4
5
6
7
8
public class UserService{

@Transactional
public void registerUser(){
userDao.save(user);
}

}

加上 @Transactional 注解等价代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class UserService {

public void registerUser(User user) {
Connection connection = dataSource.getConnection();
try (connection) {
connection.setAutoCommit(false);

userDao.save(user);

connection.commit();
} catch (SQLException e) {
connection.rollback();
}
}
}

这个操作,相比上面的 JDBC 操作,简便不少。操控事务的样板代码,不用在每个方法中写了,一个注解 Spring 统统搞定。

因此,Spring 的事务魔法秘密就揭开了。对加了 @Transactional的方法或者类,使用 AOP 的方式,帮你生成数据库的链接,事务开启、提交、回滚代码,仅此而已。

AOP

在深入 @Transcational 注解之前,还是要先简单介绍下 AOP 在事务上的实现,这对你理解后面的问题,大有裨益。

首先要清楚 AOP 在实现事务时,并不会改变原来类的行为,它只是生成了一个代理类。生成代理类的方式有 CGLIB、JDK 动态代理,两种代理方式各不相同,但这里不对代理方式阐述。

通过一个简单的 Demo 看下这个流程:

UserServiceregisterUser 方法开启事务。

1
2
3
4
5
6
7
@Service
public class UserService{
@Transactional
public void registerUser(){
// 注册用户代码实现
}
}

Spring 使用 AOP 为 UserService 生成代理类 UserServiceProxy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class UserServiceProxy{
public void registerUser(){
Connection connection = dataSource.getConnection();
try (connection) {
connection.setAutoCommit(false);

// 调用 UserService 的 registerUser 方法
invoke();

connection.commit();
} catch (SQLException e) {
connection.rollback();
}
}
}

UserController 中注入 UserService 对象。

1
2
3
4
5
6
7
8
9
10
@RestController
public class UserController{
@Autowired
private UserService userService;

@PostMapping("/register")
public void addUser(){
userService.registerUser();
}
}

/register 请求的流程如下:

aop.png

可以看到,Controller  实际上是调用 UserServiceProxyregisterUser 方法,然后在代理方法中操控事务,并调用真正的 UserServiceregisterUser

或许你还有个疑问:注入的是 UserService ,为什么调用的却是它的代理类?

这就涉及 Spring  的依赖注入原理,详细可以自行搜索。实际上在 UserController 中注入的是 UserServiceProxy ,而非看到的 UserService

一些疑难杂症

列举一些关于 Spring 事务的疑难杂症,也是面试的常考题。

为什么 private 方法加 @Transactional 注解不生效?

这个问题其实是和 AOP 相关的,因为 AOP 无法对 private 方法生成代理。无法代理也就意味着对 priavte 方法的调用,都是直接调用被代理的类。

为什么 final 方法加 @Transactional 注解不生效?

原理同上,还是 AOP 无法代理被 final 关键字修饰的方法和类

为什么类方法相互调用事务不生效?

事务方法 a调用同类的事务方法 b ,在外部调用 a 方法,b 方法的事务不生效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class UserService{

@Transactional
public void a(){
// do something
b();
}

@Transactional
public void b(){
// do something
}
}


public class test(){
userService.a();
}

其实这个只需要分析下调用过程就清楚了:

  1. 先调用代理类中的 a 方法,然后代理类中调用真正的 a 方法。
  2. UserServicea 方法执行过程中,发现要调用 b 方法,因此调用了自己的 b 方法。

可以看到,a 调用 b 的时候,并没有先经过代理类,而是直接在 UserService 中执行了,所以 b 的事务不会生效。

question_1.png

为什么注入自己就能解决相互调用问题?

同样是上面的代码,只需要在 UserService 中注入自己,b 的事务就生效了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class UserService{

@Autowird
UserService userService;

@Transactional
public void a(){
// do something
userService.b();
}

@Transactional
public void b(){
// do something
}
}


public class test(){
userService.a();
}

还记得上面说的依赖注入吗,这里注入自己,实际上注入的是 UserService 的代理类。因此在执行 userService.b() 这段代码时,会调用代理类的 b 方法,所以 b 的事务生效。

question_2.png


Spring 与事务
http://wszzf.top/2022/10/10/Spring 与事务/
作者
Greek
发布于
2022年10月10日
许可协议