Spring 事务机制深度解析
面试官:说说 Spring 事务的实现原理?
你:Spring 事务基于 AOP 实现,通过
@Transactional注解声明事务边界。底层通过PlatformTransactionManager统一管理,使用 ThreadLocal 绑定数据库连接到当前线程,确保同一线程内的多个数据库操作在同一事务中。面试官:那 Spring 事务的传播行为有哪些?REQUIRES_NEW 和 NESTED 有什么区别?
这个问题很多人只能背出定义,但面试官想考察的是你对实际场景的理解和性能影响的权衡。
链式追问一:事务传播行为
Section titled “链式追问一:事务传播行为”Q1:Spring 事务的传播行为有哪些?必考
Section titled “Q1:Spring 事务的传播行为有哪些?”回答要点:
事务传播行为定义: 当一个事务方法被另一个事务方法调用时,应该如何处理事务
7种传播行为:
┌─────────────────────────────────────────────────────────────┐│ REQUIRED(默认) │├─────────────────────────────────────────────────────────────┤│ 有事务 → 加入当前事务(共用一个连接) ││ 没事务 → 创建新事务 ││ 使用场景:绝大多数业务方法 ││ ││ 示例: ││ @Transactional ││ public void createOrder() { ││ orderMapper.insert(order); ││ inventoryService.deduct(itemId, quantity); ││ // 两个操作在同一事务中,要么都成功,要么都回滚 ││ } │└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐│ REQUIRES_NEW(独立事务) │├─────────────────────────────────────────────────────────────┤│ 无论如何都创建新事务,挂起当前事务(如有) ││ 新事务独立提交/回滚,不受外层事务影响 ││ 使用场景:记录操作日志(即使业务失败,日志也要保存) ││ ││ 性能影响: ││ • 挂起外层事务 → 占用连接池 ││ • 创建新连接 → 增加连接数 ││ • 两个连接并发执行 → 增加锁竞争 ││ ││ 示例: ││ @Transactional ││ public void createOrder() { ││ orderMapper.insert(order); ││ try { ││ logService.saveLog(log); // REQUIRES_NEW ││ } catch (Exception e) { ││ // 日志失败不影响订单创建 ││ } ││ } ││ ││ @Transactional(propagation = Propagation.REQUIRES_NEW) ││ public void saveLog(OperationLog log) { ││ logMapper.insert(log); // 独立事务 ││ } │└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐│ SUPPORTS(可选事务) │├─────────────────────────────────────────────────────────────┤│ 有事务 → 加入;没事务 → 非事务执行 ││ 使用场景:查询方法,有事务时希望参与,没有也行 ││ ││ 示例: ││ @Transactional(propagation = Propagation.SUPPORTS) ││ public User getUser(Long id) { ││ return userMapper.selectById(id); ││ } │└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐│ NOT_SUPPORTED(非事务执行) │├─────────────────────────────────────────────────────────────┤│ 有事务 → 挂起当前事务,非事务执行 ││ 没事务 → 非事务执行 ││ 使用场景:大量查询操作,不想加入事务减少锁竞争 │└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐│ MANDATORY(强制事务) │├─────────────────────────────────────────────────────────────┤│ 有事务 → 加入;没事务 → 抛出 IllegalTransactionStateEx ││ 使用场景:必须在事务中调用的方法(防止误用) │└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐│ NEVER(禁止事务) │├─────────────────────────────────────────────────────────────┤│ 有事务 → 抛出 IllegalTransactionStateException ││ 没事务 → 非事务执行 ││ 使用场景:明确不允许在事务中执行的方法 │└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐│ NESTED(嵌套事务) │├─────────────────────────────────────────────────────────────┤│ 有事务 → 在当前事务中创建嵌套事务(Savepoint) ││ • 嵌套事务回滚不影响外层事务(只回滚到保存点) ││ • 外层事务回滚会导致嵌套事务一起回滚 ││ 没事务 → 创建新事务(同 REQUIRED) ││ 使用场景:批量操作中部分失败不影响其他 ││ 注意:依赖数据库的 Savepoint 支持(JDBC),JPA 不支持 │└─────────────────────────────────────────────────────────────┘Q2:REQUIRES_NEW 和 NESTED 有什么区别?必考
Section titled “Q2:REQUIRES_NEW 和 NESTED 有什么区别?”核心差异对比:
REQUIRES_NEW(独立事务): ┌─────────────────────────────────────┐ │ 外层事务:BEGIN ... (挂起) │ ├─────────────────────────────────────┤ │ 新事务: BEGIN ... COMMIT/ROLLBACK│ ← 独立连接 ├─────────────────────────────────────┤ │ 外层事务:... COMMIT/ROLLBACK │ └─────────────────────────────────────┘
特点: • 两个事务完全独立,使用不同数据库连接 • 新事务提交后,外层回滚也不影响新事务 • 性能开销:挂起连接 + 创建新连接
NESTED(嵌套事务): ┌─────────────────────────────────────┐ │ 外层事务:BEGIN ... │ ├─────────────────────────────────────┤ │ 嵌套事务:SAVEPOINT sp1 │ ← 同一连接 │ ... │ │ ROLLBACK TO sp1(或提交) │ ├─────────────────────────────────────┤ │ 外层事务:... COMMIT/ROLLBACK │ └─────────────────────────────────────┘
特点: • 嵌套事务是外层事务的一部分,使用同一连接 • 嵌套事务回滚不影响外层(只回滚到保存点) • 外层事务回滚会导致嵌套事务一起回滚 • 性能开销:保存点管理(轻量)实战场景对比:
// 场景1:导入100条数据,部分失败不影响其他@Servicepublic class ImportService {
@Transactional // 外层事务 public void importData(List<Data> dataList) { for (Data data : dataList) { try { importOne(data); // 单条导入 } catch (Exception e) { log.error("导入失败: " + data.getId(), e); // 单条失败不影响其他,继续导入 } } }
// 使用 NESTED:单条失败只回滚该条,不影响整体 @Transactional(propagation = Propagation.NESTED) public void importOne(Data data) { dataMapper.insert(data); detailMapper.insertBatch(data.getDetails()); }}
// 场景2:记录操作日志,即使业务失败也要保存@Servicepublic class OrderService {
@Transactional // 外层事务 public void createOrder(Order order) { try { orderMapper.insert(order); inventoryService.deduct(order.getItemId(), order.getQuantity()); paymentService.charge(order.getUserId(), order.getAmount());
// 记录成功日志 logService.saveLog(new Log("SUCCESS", order.getId())); } catch (Exception e) { // 记录失败日志(即使事务回滚,日志也要保存) logService.saveLog(new Log("FAILED", order.getId(), e.getMessage())); throw e; // 回滚订单操作 } }}
@Servicepublic class LogService {
// 使用 REQUIRES_NEW:独立事务,不受外层影响 @Transactional(propagation = Propagation.REQUIRES_NEW) public void saveLog(OperationLog log) { logMapper.insert(log); // 外层回滚也能保存 }}性能对比:
| 维度 | REQUIRES_NEW | NESTED |
|---|---|---|
| 数据库连接数 | 2个(外层+新事务) | 1个(共享连接) |
| 连接池压力 | 高(占用额外连接) | 低 |
| 锁竞争 | 高(两个连接并发) | 低(同一连接) |
| 外层回滚影响 | 不影响新事务 | 嵌套事务一起回滚 |
| 适用场景 | 需要独立提交的操作 | 批量操作部分失败 |
链式追问二:事务隔离级别
Section titled “链式追问二:事务隔离级别”Q1:Spring 事务的隔离级别有哪些?必考
Section titled “Q1:Spring 事务的隔离级别有哪些?”隔离级别对比:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 性能 | 适用场景 |
|---|---|---|---|---|---|
| READ_UNCOMMITTED | 可能 | 可能 | 可能 | 最高 | 极少使用 |
| READ_COMMITTED | 不可能 | 可能 | 可能 | 高 | Oracle 默认;报表查询 |
| REPEATABLE_READ | 不可能 | 不可能 | 可能 | 中 | MySQL 默认;大多数场景 |
| SERIALIZABLE | 不可能 | 不可能 | 不可能 | 最低 | 金融场景;强一致性 |
代码示例:
@Servicepublic class ReportService {
// 允许不可重复读,提高查询性能 @Transactional(isolation = Isolation.READ_COMMITTED) public Report generateReport() { // 查询数据(可能读到其他事务已提交的修改) List<Order> orders = orderMapper.selectAll(); BigDecimal total = calculateTotal(orders); return new Report(orders, total); }
// 强一致性,防止幻读 @Transactional(isolation = Isolation.SERIALIZABLE) public void transfer(Long fromId, Long toId, BigDecimal amount) { Account from = accountMapper.selectById(fromId); Account to = accountMapper.selectById(toId);
if (from.getBalance().compareTo(amount) < 0) { throw new RuntimeException("余额不足"); }
accountMapper.deduct(fromId, amount); accountMapper.add(toId, amount); }}Spring 默认隔离级别:
@Transactional // 默认使用数据库隔离级别public void businessMethod() { }
// 显式指定隔离级别(覆盖数据库默认)@Transactional(isolation = Isolation.READ_COMMITTED)public void queryMethod() { }Q2:什么是脏读、不可重复读、幻读?高频
Section titled “Q2:什么是脏读、不可重复读、幻读?”三种问题详解:
1. 脏读(Dirty Read) 读到了其他事务未提交的数据
事务A:UPDATE user SET balance = 100 WHERE id = 1 (未提交) 事务B:SELECT balance FROM user WHERE id = 1 → 读到 100 事务A:ROLLBACK → 实际余额不是 100
后果:事务B基于错误数据做决策
2. 不可重复读(Non-Repeatable Read) 同一事务中,两次读取同一行数据,结果不同
事务A:SELECT balance FROM user WHERE id = 1 → 100 事务B:UPDATE user SET balance = 200 WHERE id = 1 → COMMIT 事务A:SELECT balance FROM user WHERE id = 1 → 200
后果:事务A中逻辑判断出错(第一次100,第二次200)
3. 幻读(Phantom Read) 同一事务中,两次查询结果集行数不同
事务A:SELECT * FROM user WHERE age > 20 → 10行 事务B:INSERT INTO user(age) VALUES(25) → COMMIT 事务A:SELECT * FROM user WHERE age > 20 → 11行
后果:事务A以为是10行,实际变成11行
区别: 脏读:读到未提交的数据 不可重复读:读到已提交的修改(UPDATE) 幻读:读到已提交的新增/删除(INSERT/DELETE)MySQL 解决方案:
READ COMMITTED: • 使用 MVCC(多版本并发控制) • 每次查询生成新的 Read View,读取已提交的数据 • 解决脏读,但可能不可重复读和幻读
REPEATABLE_READ(MySQL 默认): • 使用 MVCC + Next-Key Lock(间隙锁) • 事务开始时生成 Read View,后续查询复用 • 解决脏读、不可重复读 • 通过间隙锁防止幻读(锁定查询范围)
SERIALIZABLE: • 所有读操作加共享锁 • 完全串行化,无并发问题 • 性能最差链式追问三:@Transactional 失效场景
Section titled “链式追问三:@Transactional 失效场景”Q1:@Transactional 有哪些常见的失效场景?必考
Section titled “Q1:@Transactional 有哪些常见的失效场景?”失效场景总结:
┌─────────────────────────────────────────────────────────────┐│ 1. 自调用(同类方法调用) │├─────────────────────────────────────────────────────────────┤│ @Service ││ public class OrderService { ││ public void outer() { ││ this.inner(); // 通过 this 调用,绕过代理 ││ } ││ ││ @Transactional ││ public void inner() { ... } ││ } ││ ││ 根本原因:@Transactional 依赖 AOP 代理,自调用绕过代理 ││ 解决方案:注入自身、重构代码提取到另一个 Bean │└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐│ 2. 方法不是 public │├─────────────────────────────────────────────────────────────┤│ @Service ││ public class OrderService { ││ @Transactional ││ protected void createOrder() { ... } // 失效! ││ } ││ ││ 根本原因:Spring AOP 只能代理 public 方法 ││ CGLIB 无法重写非 public 方法 │└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐│ 3. 异常被吞掉(catch 后未重新抛出) │├─────────────────────────────────────────────────────────────┤│ @Transactional ││ public void createOrder() { ││ try { ││ orderMapper.insert(order); ││ inventoryService.deduct(itemId, quantity); ││ } catch (Exception e) { ││ log.error("出错了", e); ││ // 没有重新抛出异常! ││ // Spring 认为方法正常执行,提交事务 ││ } ││ } │└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐│ 4. 异常类型不匹配(默认只回滚 RuntimeException) │├─────────────────────────────────────────────────────────────┤│ @Transactional ││ public void createOrder() throws Exception { ││ orderMapper.insert(order); ││ throw new Exception("受检异常"); // 不会回滚! ││ } ││ ││ 正确写法: ││ @Transactional(rollbackFor = Exception.class) // 推荐 ││ public void createOrder() throws Exception { ... } │└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐│ 5. 数据库引擎不支持事务 │├─────────────────────────────────────────────────────────────┤│ MySQL 的 MyISAM 不支持事务,只有 InnoDB 支持 ││ Spring 事务无法生效 │└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐│ 6. Bean 未被 Spring 管理 │├─────────────────────────────────────────────────────────────┤│ public class OrderController { ││ public void test() { ││ OrderService orderService = new OrderService(); ││ orderService.createOrder(); // 未被 Spring 管理 ││ } ││ } │└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐│ 7. 多线程操作 │├─────────────────────────────────────────────────────────────┤│ @Transactional ││ public void process() { ││ new Thread(() -> { ││ // 新线程中的操作不在当前事务中 ││ // Spring 事务通过 ThreadLocal 绑定连接 ││ userMapper.update(user); ││ }).start(); ││ } │└─────────────────────────────────────────────────────────────┘代码示例:正确的事务用法:
@Servicepublic class OrderService {
// 推荐写法:public + rollbackFor @Transactional(rollbackFor = Exception.class) public void createOrder(Order order) { // 1. 插入订单 orderMapper.insert(order);
// 2. 扣减库存 int affected = inventoryMapper.deduct(order.getItemId(), order.getQuantity()); if (affected == 0) { throw new RuntimeException("库存不足"); // 抛出 RuntimeException,触发回滚 }
// 3. 扣款 paymentService.charge(order.getUserId(), order.getAmount());
// 4. 记录日志(独立事务,即使订单失败也要保存) try { logService.saveLog(new Log("CREATE_ORDER", order.getId())); } catch (Exception e) { log.error("记录日志失败", e); // 日志失败不影响订单创建 } }}
@Servicepublic class LogService {
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class) public void saveLog(OperationLog log) { logMapper.insert(log); }}Q2:为什么 @Transactional 默认只回滚 RuntimeException?高频
Section titled “Q2:为什么 @Transactional 默认只回滚 RuntimeException?”设计理念:
Java 异常体系:
Throwable ├── Error(错误,程序无法处理) │ └── VirtualMachineError、OutOfMemoryError... └── Exception(异常) ├── RuntimeException(运行时异常,未检查) │ └── NullPointerException、IllegalArgumentException... └── Checked Exception(受检异常,必须处理) └── IOException、SQLException...
Spring 设计选择: RuntimeException: • 编程错误,不可恢复 • 应该回滚事务
Checked Exception: • 预期内的业务错误(如用户输入验证失败) • 可能需要捕获并处理 • 不应该自动回滚
实际建议: 始终指定 rollbackFor = Exception.class 避免"忘记指定导致受检异常时未回滚"的 Bug代码示例:
// 错误示例:受检异常不会回滚@Transactionalpublic void createOrder() throws IOException { orderMapper.insert(order); throw new IOException("文件读取失败"); // 不会回滚!}
// 正确示例:指定 rollbackFor@Transactional(rollbackFor = Exception.class)public void createOrder() throws IOException { orderMapper.insert(order); throw new IOException("文件读取失败"); // 会回滚}
// 特殊场景:只回滚特定异常@Transactional(rollbackFor = {IOException.class, SQLException.class})public void process() throws Exception { // IOException、SQLException 会回滚 // 其他异常不回滚}链式追问四:声明式 vs 编程式事务
Section titled “链式追问四:声明式 vs 编程式事务”Q1:声明式事务和编程式事务有什么区别?高频
Section titled “Q1:声明式事务和编程式事务有什么区别?”对比表格:
| 对比项 | 声明式事务(@Transactional) | 编程式事务(TransactionTemplate) |
|---|---|---|
| 代码侵入 | 无(注解驱动) | 有(需注入 TransactionTemplate) |
| 事务粒度 | 方法级别 | 代码块级别(更精细) |
| 可读性 | 高(简洁) | 低(样板代码) |
| 失效风险 | 高(AOP 相关失效场景) | 低(直接控制) |
| 适用场景 | 绝大多数业务 | 需要精确控制事务边界 |
代码示例:
// 声明式事务@Servicepublic class OrderService {
@Transactional(rollbackFor = Exception.class) public void transfer(Long fromId, Long toId, BigDecimal amount) { accountMapper.deduct(fromId, amount); accountMapper.add(toId, amount); }}
// 编程式事务@Servicepublic class OrderService {
@Autowired private TransactionTemplate transactionTemplate;
@Autowired private AccountMapper accountMapper;
public void transfer(Long fromId, Long toId, BigDecimal amount) { transactionTemplate.execute(status -> { try { accountMapper.deduct(fromId, amount); accountMapper.add(toId, amount); return null; // 正常返回,提交事务 } catch (Exception e) { status.setRollbackOnly(); // 标记回滚 throw e; } }); }}什么时候用编程式事务:
// 场景1:需要比方法更细的事务粒度@Servicepublic class BatchService {
@Autowired private TransactionTemplate transactionTemplate;
public void importData(List<Data> dataList) { for (Data data : dataList) { // 每条数据独立事务 transactionTemplate.execute(status -> { try { dataMapper.insert(data); detailMapper.insertBatch(data.getDetails()); return null; } catch (Exception e) { status.setRollbackOnly(); log.error("导入失败: " + data.getId(), e); return null; // 单条失败不影响其他 } }); } }}
// 场景2:存在 AOP 自调用问题且不方便重构@Servicepublic class OrderService {
@Autowired private TransactionTemplate transactionTemplate;
public void process() { // 非事务操作 validateOrder();
// 事务操作 transactionTemplate.execute(status -> { createOrder(); deductInventory(); return null; });
// 非事务操作 sendNotification(); }}Q2:Spring 事务如何保证线程安全?中频
Section titled “Q2:Spring 事务如何保证线程安全?”核心机制:ThreadLocal:
Spring 事务架构:
PlatformTransactionManager(事务管理器) ↓ TransactionSynchronizationManager(事务同步管理器) ↓ ThreadLocal<Map<Object, Object>> resources ↓ key: DataSource → value: ConnectionHolder(数据库连接)
工作流程:
1. 事务开始: • 从数据源获取 Connection • 绑定到 ThreadLocal(key=DataSource, value=ConnectionHolder) • 设置 Connection.setAutoCommit(false)
2. 同一线程内的数据库操作: • MyBatis/JDBC 从 ThreadLocal 获取 Connection • 使用同一个 Connection 执行 SQL • 保证同一事务
3. 事务结束: • Connection.commit() 或 Connection.rollback() • 清除 ThreadLocal 中的 ConnectionHolder
关键理解: • 不同线程有不同 ThreadLocal,无法共享事务 • 这就是为什么多线程操作不在同一事务中 • 事务传播能工作(同一线程,嵌套调用共享同一 ThreadLocal)源码示例:
// TransactionSynchronizationManager 源码(简化)public abstract class TransactionSynchronizationManager {
// ThreadLocal 存储数据库连接 private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal<>("Transactional resources");
// 绑定连接到当前线程 public static void bindResource(Object key, Object value) { Map<Object, Object> map = resources.get(); if (map == null) { map = new HashMap<>(); resources.set(map); } map.put(key, value); }
// 从当前线程获取连接 public static Object getResource(Object key) { Map<Object, Object> map = resources.get(); if (map == null) { return null; } return map.get(key); }
// 清除连接 public static void unbindResource(Object key) { Map<Object, Object> map = resources.get(); if (map != null) { map.remove(key); if (map.isEmpty()) { resources.remove(); } } }}高频面试题速查
Section titled “高频面试题速查”Q:Spring 事务的传播行为有哪些?
7种:REQUIRED(默认,有则加入无则新建)、REQUIRES_NEW(始终新建独立事务)、SUPPORTS(有则加入无则非事务)、NOT_SUPPORTED(始终非事务挂起当前)、MANDATORY(必须有事务无则抛异常)、NEVER(必须无事务有则抛异常)、NESTED(嵌套事务基于 Savepoint)。
Q:REQUIRED 和 REQUIRES_NEW 的区别?
REQUIRED 加入当前事务(共用同一连接),两者共进退;REQUIRES_NEW 挂起当前事务,创建全新独立事务(新连接),独立提交或回滚,外层回滚不影响已提交的新事务。性能上 REQUIRES_NEW 占用额外连接,增加连接池压力。
Q:@Transactional 失效场景?
自调用(绕过代理)、方法非 public、异常被 catch 吞掉、抛出受检异常未指定 rollbackFor、数据库引擎不支持事务(MyISAM)、Bean 未被 Spring 管理、多线程操作(新线程不继承事务)。
Q:为什么默认只回滚 RuntimeException?
Spring 设计理念:RuntimeException 是编程错误不可恢复应回滚;受检异常是预期业务错误可捕获处理不应自动回滚。实际推荐始终指定 rollbackFor = Exception.class 避免遗漏。
Q:声明式事务 vs 编程式事务?
声明式:注解驱动,代码简洁,事务粒度到方法,受 AOP 失效影响。编程式:TransactionTemplate,代码侵入,事务粒度到代码块,精确控制不受 AOP 限制。需要细粒度控制或避免 AOP 问题时用编程式。