Spring事务面试准备

63 阅读5分钟

Spring 事务传播机制

Spring 事务传播机制可使用 @Transactional(propagation=Propagation.REQUIRED) 来定义,Spring 事务传播机制的级别包含以下 7 种:

  • Propagation.REQUIRED:默认的事务传播级别,
    • 如果当前存在事务,则加入该事务;
    • 如果当前没有事务,则创建一个新的事务。
  • Propagation.SUPPORTS:
    • 如果当前存在事务,则加入该事务;
    • 如果当前没有事务,则以非事务的方式继续运行。
  • Propagation.MANDATORY:(mandatory:强制性)
    • 如果当前存在事务,则加入该事务;
    • 如果当前没有事务,则抛出异常。
  • Propagation.REQUIRES_NEW:表示创建一个新的事务
    • 如果当前存在事务,则把当前事务挂起。
    • 也就是说不管外部方法是否开启事务,Propagation.REQUIRES_NEW 修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。
  • Propagation.NOT_SUPPORTED:以非事务方式运行
    • 如果当前存在事务,则把当前事务挂起。
  • Propagation.NEVER:以非事务方式运行
    • 如果当前存在事务,则抛出异常。
  • Propagation.NESTED:
    • 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;
    • 嵌套事务发生异常后会局部回滚,不会影响外部事务(但外部事务会影响嵌套事务);
    • 如果当前没有事务,则创建一个新的事务。

嵌套事务 NESTED 的回滚策略(关键逻辑)

  • NESTED 内部出现异常 → 仅仅回滚到其保存点
  • 主事务 T1 中之前的操作 可以不受影响

示例:

T1:
  A()  // 正常
  B() nested // 内部异常,只回滚 B 的保存点

A() 的数据可以保留。

但是重要前提:

如果异常继续往外抛出 → 最终 T1 也会回滚!!!

所以 NESTED 的效果完全依赖于外层方法是否捕获异常。

异常传播对 NESTED 的影响(极重要)

NESTED 的“局部回滚能力”只有在以下条件中才能生效:

只有在内部 try-catch 掉异常时,NESTED 才能保持外层事务不回滚

否则异常继续抛出:

NESTED → 外层 → 最终 T1 肯定回滚

所以:

正确用法:

try {
    logService.saveLog();  // nested
} catch(Exception e) {
    // swallow
}

错误用法:

logService.saveLog(); // nested,但异常向外抛出 → 外层 T1 一定回滚

REQUIRED:所有操作绑定在一个事务中,只能整体成功或整体失败。

NESTED:在主事务中划分可单独回滚的“子单元”,可实现“局部失败、整体成功”,但前提是异常必须在内部捕获。

Spring 的核心机制:异常未被处理 → 事务标记 rollback-only ; 无论传播行为是 REQUIRED、REQUIRES

Spring Data JPA 不支持 Propagation.NESTED

Hibernate(JPA 默认实现)不支持保存点(Savepoint)机制,因此无法真正实现嵌套事务。

在 JPA 环境中使用 @Transactional(propagation = Propagation.NESTED) Spring 会尝试创建一个 savepoint,但 HibernateTransactionManager 没有实现该能力。- 要么直接退化成 REQUIRED(加入同一个事务)要么抛出异常:不支持 savepoint

JPA 要想做到子操作失败不影响主事务,唯一可行方式是:使用 REQUIRES_NEW 代替 NESTED

隔离级别

  • SERIALIZABLE 串行化
    • 避免所有并发问题
    • 性能最差
  • REPEATABLE READ(可重复读)
    • MySQL 默认的隔离级别
    • 避免脏读、不可重复读;可能会出现幻读
    • MySQL 中使用 MVCC + Next-Key Lock 可以避免幻读
  • READ COMMITTED(读已提交)
    • 避免脏读;可能出现 不可重复读和幻读
  • READ UNCOMMITTED(读未提交)
    • 脏读、不可重复读、幻读都会发生

READ COMMITTED(读已提交)

同一事务内:

  • 每次查询都能看到别人已经提交的数据(每次读都创建新的快照)
  • 不会脏读
  • 会出现不可重复读
  • 会出现幻读

事务A:

BEGIN;

SELECT balance FROM account WHERE id = 1;  -- 读到 100

事务B:

UPDATE account SET balance = 200 WHERE id = 1;
COMMIT;

事务A 再查:

SELECT balance FROM account WHERE id = 1;  -- 这次读到 200

同一事务内两次读取结果不一致,这就是 不可重复读

REPEATABLE READ(可重复读)

同一事务内:

  • 第一次查询创建快照(Snapshot) ,后续读都基于这个快照
  • 即使别的事务提交了数据,当前事务也看不到
  • 不可重复读不会发生
  • MySQL InnoDB 中不会发生幻读

事务A:

BEGIN;

SELECT balance FROM account WHERE id = 1;  -- 读到 100

事务B:

UPDATE account SET balance = 200 WHERE id = 1;
COMMIT;

事务A 再查:

SELECT balance FROM account WHERE id = 1;  -- 仍然读到 100

结果一致,可重复读实现了。

MySQL 为什么在可重复读下没有幻读?

多数数据库 REPEATABLE READ 会出现幻读,但 MySQL InnoDB 使用 MVCC + Next-Key Lock(间隙锁) 解决了幻读问题

事务A:

BEGIN;

SELECT * FROM orders WHERE amount > 100;

事务B 插入符合条件的数据:

INSERT INTO orders (amount) VALUES (200);
COMMIT;

事务A 再查:

SELECT * FROM orders WHERE amount > 100;

在 MySQL 中,事务A 看不到事务B插入的新行,因为间隙锁阻止了产生幻读。

场景选择

使用 READ COMMITTED 的场景

  • 高并发 OLTP 系统(避免长时间持锁)
  • 查询结果允许变化(比如订单列表、日志、动态内容)
  • 避免锁争用、提高吞吐量更重要

使用 REPEATABLE READ 的场景

  • 金融类、余额扣减类场景
  • 需要确保“事务内看到的数据一致”
  • 需要避免不可重复读的情况

资料来源