Skip to content

Spring 事务机制深度解析

面试官:说说 Spring 事务的实现原理?

:Spring 事务基于 AOP 实现,通过 @Transactional 注解声明事务边界。底层通过 PlatformTransactionManager 统一管理,使用 ThreadLocal 绑定数据库连接到当前线程,确保同一线程内的多个数据库操作在同一事务中。

面试官:那 Spring 事务的传播行为有哪些?REQUIRES_NEW 和 NESTED 有什么区别?

这个问题很多人只能背出定义,但面试官想考察的是你对实际场景的理解性能影响的权衡


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条数据,部分失败不影响其他
@Service
public 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:记录操作日志,即使业务失败也要保存
@Service
public 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; // 回滚订单操作
}
}
}
@Service
public class LogService {
// 使用 REQUIRES_NEW:独立事务,不受外层影响
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveLog(OperationLog log) {
logMapper.insert(log); // 外层回滚也能保存
}
}

性能对比

维度REQUIRES_NEWNESTED
数据库连接数2个(外层+新事务)1个(共享连接)
连接池压力高(占用额外连接)
锁竞争高(两个连接并发)低(同一连接)
外层回滚影响不影响新事务嵌套事务一起回滚
适用场景需要独立提交的操作批量操作部分失败

Q1:Spring 事务的隔离级别有哪些?必考

Section titled “Q1:Spring 事务的隔离级别有哪些?”

隔离级别对比

隔离级别脏读不可重复读幻读性能适用场景
READ_UNCOMMITTED可能可能可能最高极少使用
READ_COMMITTED不可能可能可能Oracle 默认;报表查询
REPEATABLE_READ不可能不可能可能MySQL 默认;大多数场景
SERIALIZABLE不可能不可能不可能最低金融场景;强一致性

代码示例

@Service
public 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(); │
│ } │
└─────────────────────────────────────────────────────────────┘

代码示例:正确的事务用法

@Service
public 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);
// 日志失败不影响订单创建
}
}
}
@Service
public 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

代码示例

// 错误示例:受检异常不会回滚
@Transactional
public 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 相关失效场景)低(直接控制)
适用场景绝大多数业务需要精确控制事务边界

代码示例

// 声明式事务
@Service
public class OrderService {
@Transactional(rollbackFor = Exception.class)
public void transfer(Long fromId, Long toId, BigDecimal amount) {
accountMapper.deduct(fromId, amount);
accountMapper.add(toId, amount);
}
}
// 编程式事务
@Service
public 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:需要比方法更细的事务粒度
@Service
public 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 自调用问题且不方便重构
@Service
public 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();
}
}
}
}

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 问题时用编程式。