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 |
|
上诉操作完成了一个事务的提交,倘若要回滚上诉操作只需要将 COMMIT
替换成 ROLLBACK
。
1 |
|
不知道你发现了没有,提交和回滚都是针对一组 SQL 进行的。用户注册账号成功,但是充值失败,能否让「 充值失败」不影响到用户注册。
答案是肯定的,上面提到的 SAVEPOINT
就是解决该问题的。
SAVEPOINT
就像游戏存档一样,可以在事务的执行过程中建立多个存档,遇到异常可以随时返回到指定的存档。如下面的语句,「 注册」会成功,而「充值」失败。
1 |
|
传统的 JDBC 管理事务
提交与回滚操作
看下这段代码,你是否熟悉。
1 |
|
- 获取数据库连接,获取的方式有多种,现在大多数都是维护一个数据库连接池,然后从连接池分配一个连接。
- 把获取到的数据库连接,关闭自动提交。因为事务要交由代码管理,而不是让数据库默认提交。
- 当执行完 SQL 代码之后,开始提交。
- 数据库进行 COMMIT 提交出现异常,代码中进行捕获,并执行回滚操作。
设置隔离级别与保留点(SAVEPOINT)
在 jdbc 中设置数据库隔离级别和 SAVEPOINT
也是非常简单。
1 |
|
setTransactionIsolation
api 就可以设置数据库的隔离级别setSavepoint
创建一个SAVEPOINT
rollback
到上一个SAVEPOINT
可以看到 jdbc 中对数据库事务的操作都是非常简单的,Spring 与 jdbc 实现事务的操作并无太大差别,只是他把这些封装的太好,会让你觉得是魔法,难以理解。
Spring 的事务魔法
Transactional 注解
使用 JDBC 开启事务,需要写大量的 try...catch
。通常 try
代码块执行 SQL 操作,catch
中捕获异常进行回滚。
来看下 Spring 中为一个方法添加事务有多简单
1 |
|
加上 @Transactional
注解等价代码如下:
1 |
|
这个操作,相比上面的 JDBC 操作,简便不少。操控事务的样板代码,不用在每个方法中写了,一个注解 Spring 统统搞定。
因此,Spring 的事务魔法秘密就揭开了。对加了 @Transactional
的方法或者类,使用 AOP 的方式,帮你生成数据库的链接,事务开启、提交、回滚代码,仅此而已。
AOP
在深入 @Transcational
注解之前,还是要先简单介绍下 AOP 在事务上的实现,这对你理解后面的问题,大有裨益。
首先要清楚 AOP 在实现事务时,并不会改变原来类的行为,它只是生成了一个代理类。生成代理类的方式有 CGLIB、JDK 动态代理,两种代理方式各不相同,但这里不对代理方式阐述。
通过一个简单的 Demo 看下这个流程:
在 UserService
的 registerUser
方法开启事务。
1 |
|
Spring 使用 AOP 为 UserService
生成代理类 UserServiceProxy
。
1 |
|
在 UserController
中注入 UserService
对象。
1 |
|
/register
请求的流程如下:
可以看到,Controller
实际上是调用 UserServiceProxy
的 registerUser
方法,然后在代理方法中操控事务,并调用真正的 UserService
的 registerUser
。
或许你还有个疑问:注入的是 UserService
,为什么调用的却是它的代理类?
这就涉及 Spring 的依赖注入原理,详细可以自行搜索。实际上在 UserController
中注入的是 UserServiceProxy
,而非看到的 UserService
。
一些疑难杂症
列举一些关于 Spring 事务的疑难杂症,也是面试的常考题。
为什么 private
方法加 @Transactional
注解不生效?
这个问题其实是和 AOP 相关的,因为 AOP 无法对 private
方法生成代理。无法代理也就意味着对 priavte
方法的调用,都是直接调用被代理的类。
为什么 final
方法加 @Transactional
注解不生效?
原理同上,还是 AOP 无法代理被 final
关键字修饰的方法和类
为什么类方法相互调用事务不生效?
事务方法 a
调用同类的事务方法 b
,在外部调用 a
方法,b
方法的事务不生效。
1 |
|
其实这个只需要分析下调用过程就清楚了:
- 先调用代理类中的
a
方法,然后代理类中调用真正的a
方法。 UserService
的a
方法执行过程中,发现要调用b
方法,因此调用了自己的b
方法。
可以看到,a
调用 b
的时候,并没有先经过代理类,而是直接在 UserService
中执行了,所以 b
的事务不会生效。
为什么注入自己就能解决相互调用问题?
同样是上面的代码,只需要在 UserService
中注入自己,b
的事务就生效了。
1 |
|
还记得上面说的依赖注入吗,这里注入自己,实际上注入的是 UserService
的代理类。因此在执行 userService.b()
这段代码时,会调用代理类的 b
方法,所以 b
的事务生效。