SpringBoot 的事务管理
1、事务的概念
事务,指的是一组操作,要么全部成功,要么全部失败,不存在部分成功部分失败的情况。
以游戏充钱为例,分解为下面两组操作:
- 游戏内点击购买 648 点券 后 微信成功扣款 648 元;
- 游戏运营商确定收款 648 元后支付 648 点券到你的游戏账户(著名的马学家讲过,你的钱只是换了种方式陪在你身边)。
这上面两个操作必须一起成功,或者一起失败,不然就乱套了。
例如你微信扣钱了,但实际上点券没到账,岂不是成冤大头了。
这个时候,就需要由事务来进行保证全部操作的一致。
事务有 4 个特性,简称 ACID :
- 原子性(Atomicity):一组操作要么全做,要么全不做。
- 一致性(Consistency):让数据库从一个状态正常过度到另一个状态。
- 隔离性(Isolation):多组事务同时进行互相不干扰。
- 持久性(Durability):事务提交后,数据库能够永久保存,不会丢失。
大致流程如下:
2、@Transactional 注解的使用
SpringBoot 提供了好几种方式来管理事务,最常用也最简单的就是通过 @Transactional 注解实现。而 @Transactional 实际是利用了 TransactionManager 进行事务的管理。
以上面充值游戏点券为例,java 代码如下:
public class GameService {
@Transactional
public String getGameMoney() {
//微信支出 648
//游戏账户点券增加 648
return "gameMoney";
}
}
注释里的所有操作都被包含在一个事务中。
知道了事务最简单的用法,还得回头来了解下事务中两个很重要的要点,一个叫传播行为,一个叫隔离等级。
3、事务的传播行为
事务就像个保护罩,把操作裹起来,确保要么全成,要么全挂。
而传播行为(Propagation Behavior),讲的是一个罩着保护罩的方法 A,去调用另一个方法 B 时,这罩子咋传过去的问题。
Spring 事务传播机制的级别包含以下 7 种:
- Propagation.REQUIRED:默认的事务传播级别,它表示如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
- Propagation.SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
- Propagation.MANDATORY:(mandatory:强制性)如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
- Propagation.REQUIRES_NEW:表示创建一个新的事务,如果当前存在事务,则把当前事务挂起。也就是说不管外部方法是否开启事务,Propagation.REQUIRES_NEW 修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。
- Propagation.NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。
- Propagation.NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。
- Propagation.NESTED:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于 PROPAGATION_REQUIRED。
以上 7 种传播机制,可根据“是否支持当前事务”的维度分为以下 3 类:
以情侣之间是否要买房为例,我们将以上 3 类事务传播机制可以看作是恋爱中的 3 类女生类型:
- 普通型
- 强势型
- 懂事型 这三类女生如下图所示:
4、事务的隔离级别
隔离级别,就是解决多个事务一块跑时互相干扰的问题,比如脏读、不可重复读、幻读这些麻烦。
隔离级别越高,数据越安全,但并发性能可能越差(因为限制更多了)。
Spring支持的标准隔离级别:
| 级别 | 脏读 | 不可重复读 | 幻读 | 性能 |
|---|---|---|---|---|
| 读未提交(READ_UNCOMMITTED) | ✓ | ✓ | ✓ | 最好 |
| 读已提交(READ_COMMITTED) | × | ✓ | ✓ | 好 |
| 可重复读(REPEATABLE_READ) | × | × | ✓ | 一般 |
| 串行化(SERIALIZABLE) | × | × | × | 最差 |
READ_UNCOMMITTED(读未提交) :
- 隔离级别最低,几乎没有隔离。
- 可能发生脏读、不可重复读、幻读。
- 性能最好,但数据最不安全。
- 类比:可以随便看别人正在写的草稿。
READ_COMMITTED(读已提交) :
- 保证只能读到已经提交的数据,解决了脏读问题。
- 但还可能发生不可重复读和幻读。
- 这是大多数数据库(如 Oracle, SQL Server, PostgreSQL)的默认级别。
REPEATABLE_READ(可重复读) :
- 保证在一个事务内多次读取同一数据时,结果总是一致的,解决了不可重复读问题。
- 但仍可能发生幻读(理论上,但 MySQL InnoDB 通过 MVCC 和间隙锁解决了幻读)。
- 这是 MySQL 的默认隔离级别。
SERIALIZABLE(串行化) :
- 隔离级别最高,强制事务串行执行(一个接一个),避免了所有并发问题。
- 但性能最差,因为失去了并发性。
- 类比:大家排队,一个一个来。
5、注意事项
-
方法可见性:只对
public方法生效:@Transactional加在private、protected或package-private方法上是无效的,且 Spring 不会报错(静默失败)。- 原理:Spring 事务是基于 AOP 代理实现的,非
public方法无法被代理类有效拦截。 - 记住:事务方法必须是公开的(public)!
-
异常处理:默认只认
RuntimeException和Error- 默认情况下,只有当方法抛出
RuntimeException或Error时,事务才会回滚。 - 如果你抛出的是受检异常(Checked Exception,比如
IOException,SQLException),事务不会回滚! - 得用
@Transactional(rollbackFor=Exception.class)才管用 - 或者
rollbackFor = {SpecificException.class, ...}指定特定异常回滚 - 也可以用
noRollbackFor来指定哪些异常不回滚
- 默认情况下,只有当方法抛出
-
自调用问题:同一个类内部调用会失效
-
在一个 Service 类里面,一个没有
@Transactional注解的方法 A 调用同一个类里面另一个有@Transactional注解的方法 B,方法 B 的事务不会生效。 -
因为 Spring AOP 代理是基于目标对象的代理实例。当你在类内部直接调用
this.methodB()时,是直接调用原始对象的方法,绕过了代理对象,自然事务拦截器就没机会工作了。 -
解决方案
:
- 注入自己代理对象:通过
ApplicationContext获取自身的代理 Bean,再用代理对象调用。 - 将事务方法移到另一个 Bean 中,通过 Bean 注入调用。
- 使用
AspectJ(配置更复杂)。
- 注入自己代理对象:通过
-
-
事务超时:防止长时间锁定资源
- 可以通过
@Transactional(timeout = 10)设置事务超时时间(单位秒)。如果事务执行时间超过设定值,会自动回滚并抛出异常。 - 有助于防止某个事务长时间占用数据库连接或锁,影响系统性能。
- 可以通过
-
只读事务:优化查询性能
- 对于只有查询操作的方法,可以设置
@Transactional(readOnly = true)。 - 这会告诉数据库这是一个只读操作,数据库可以进行一些性能优化,比如不记录回滚日志。同时也能在某些隔离级别下防止误操作(如尝试更新)。
- 对于只有查询操作的方法,可以设置
-
多数据源事务:需要特殊处理
- 需要分布式事务
参考资料