SpringBoot 的事务管理

49 阅读6分钟

SpringBoot 的事务管理

1、事务的概念

事务,指的是一组操作,要么全部成功,要么全部失败,不存在部分成功部分失败的情况。

以游戏充钱为例,分解为下面两组操作:

  1. 游戏内点击购买 648 点券 后 微信成功扣款 648 元;
  2. 游戏运营商确定收款 648 元后支付 648 点券到你的游戏账户(著名的马学家讲过,你的钱只是换了种方式陪在你身边)。

这上面两个操作必须一起成功,或者一起失败,不然就乱套了。

例如你微信扣钱了,但实际上点券没到账,岂不是成冤大头了。

这个时候,就需要由事务来进行保证全部操作的一致。

事务有 4 个特性,简称 ACID :

  • 原子性(Atomicity):一组操作要么全做,要么全不做。
  • 一致性(Consistency):让数据库从一个状态正常过度到另一个状态。
  • 隔离性(Isolation):多组事务同时进行互相不干扰。
  • 持久性(Durability):事务提交后,数据库能够永久保存,不会丢失。

大致流程如下:

事务概念.drawio

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 种:

  1. Propagation.REQUIRED:默认的事务传播级别,它表示如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
  2. Propagation.SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
  3. Propagation.MANDATORY:(mandatory:强制性)如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
  4. Propagation.REQUIRES_NEW:表示创建一个新的事务,如果当前存在事务,则把当前事务挂起。也就是说不管外部方法是否开启事务,Propagation.REQUIRES_NEW 修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。
  5. Propagation.NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。
  6. Propagation.NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。
  7. Propagation.NESTED:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于 PROPAGATION_REQUIRED。

以上 7 种传播机制,可根据“是否支持当前事务”的维度分为以下 3 类:

image

以情侣之间是否要买房为例,我们将以上 3 类事务传播机制可以看作是恋爱中的 3 类女生类型:

  • 普通型
  • 强势型
  • 懂事型 这三类女生如下图所示:

image-1

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、注意事项

  1. 方法可见性:只对 public 方法生效

    • @Transactional 加在 privateprotectedpackage-private 方法上是无效的,且 Spring 不会报错(静默失败)。
    • 原理:Spring 事务是基于 AOP 代理实现的,非 public 方法无法被代理类有效拦截。
    • 记住:事务方法必须是公开的(public)!
  2. 异常处理:默认只认 RuntimeExceptionError

    • 默认情况下,只有当方法抛出 RuntimeExceptionError 时,事务才会回滚。
    • 如果你抛出的是受检异常(Checked Exception,比如 IOException, SQLException),事务不会回滚!
    • 得用 @Transactional(rollbackFor=Exception.class) 才管用
    • 或者 rollbackFor = {SpecificException.class, ...} 指定特定异常回滚
    • 也可以用 noRollbackFor 来指定哪些异常不回滚
  3. 自调用问题:同一个类内部调用会失效

    • 在一个 Service 类里面,一个没有 @Transactional 注解的方法 A 调用同一个类里面另一个有 @Transactional 注解的方法 B,方法 B 的事务不会生效。

    • 因为 Spring AOP 代理是基于目标对象的代理实例。当你在类内部直接调用 this.methodB() 时,是直接调用原始对象的方法,绕过了代理对象,自然事务拦截器就没机会工作了。

    • 解决方案

      • 注入自己代理对象:通过 ApplicationContext 获取自身的代理 Bean,再用代理对象调用。
      • 将事务方法移到另一个 Bean 中,通过 Bean 注入调用。
      • 使用 AspectJ (配置更复杂)。
  4. 事务超时:防止长时间锁定资源

    • 可以通过 @Transactional(timeout = 10) 设置事务超时时间(单位秒)。如果事务执行时间超过设定值,会自动回滚并抛出异常。
    • 有助于防止某个事务长时间占用数据库连接或锁,影响系统性能。
  5. 只读事务:优化查询性能

    • 对于只有查询操作的方法,可以设置 @Transactional(readOnly = true)
    • 这会告诉数据库这是一个只读操作,数据库可以进行一些性能优化,比如不记录回滚日志。同时也能在某些隔离级别下防止误操作(如尝试更新)。
  6. 多数据源事务:需要特殊处理

    • 需要分布式事务

参考资料