Appearance
声明式事务
Spring 的声明式事务通过 @Transactional 注解,以 AOP 的方式将事务管理从业务代码中分离。你需要理解的是它背后的原理,而不是怎么加注解。
事务基础概念(ACID、隔离级别、MVCC)请参阅 Java 知识库-事务处理;分布式事务请参阅 分布式事务与实现。
一、声明式事务原理
@Transactional 的底层是 AOP:
调用 orderService.createOrder()
│
▼
┌─────────────────────────────────────────────┐
│ 代理对象(CGLIB 动态代理) │
│ │
│ TransactionInterceptor │
│ ├─ ① 获取 DataSource │
│ ├─ ② 绑定连接到当前线程(ThreadLocal) │
│ ├─ ③ 关闭自动提交(autoCommit = false) │
│ ├─ ④ 执行目标方法 │
│ ├─ ⑤ 成功 → commit() │
│ └─ ⑥ 异常 → rollback() │
└─────────────────────────────────────────────┘Spring 事务核心抽象:
① PlatformTransactionManager(事务管理器)
不同数据源有不同的实现:
DataSourceTransactionManager → JDBC
JpaTransactionManager → JPA
HibernateTransactionManager → Hibernate
JtaTransactionManager → 分布式事务(JTA)
② TransactionDefinition(事务定义)
传播行为、隔离级别、超时时间、只读标志
③ TransactionStatus(事务状态)
是否新事务、是否已完成、是否有保存点二、核心属性
java
@Transactional(
propagation = Propagation.REQUIRED, // 传播机制(默认)
isolation = Isolation.REPEATABLE_READ, // 隔离级别
timeout = 30, // 超时时间(秒),-1 表示不超时
readOnly = false, // 是否只读(优化提示)
rollbackFor = Exception.class, // 哪些异常回滚
noRollbackFor = IllegalArgumentException.class // 哪些异常不回滚
)
public void createOrder(Order order) { }2.1 为什么需要传播机制?
传播机制解决一个真实场景下的矛盾:多个 Service 互相调用时,事务粒度怎么划分?
假设没有传播机制,每个 @Transactional 方法都独立开事务:
java
@Service
public class OrderService {
@Transactional
public void createOrder(Order order) {
orderMapper.insert(order); // 事务 A
paymentService.pay(order); // 事务 B —— 独立事务!
inventoryService.deduct(order); // 事务 C —— 独立事务!
}
}问题:
createOrder 执行:
┌── 事务 A:insert 订单 ── 提交 ✅
└── 事务 B:pay 支付
└── 扣款成功,提交 ✅
└── 事务 C:deduct 扣库存
└── 库存不足,回滚 ❌
结果:订单创建了,钱扣了,但库存没扣!这是灾难。传播机制的本质是"事务边界控制"——允许你在方法调用链上精确控制:哪些操作共享同一个事务(一起成败),哪些操作独立开事务(互不干扰),哪些操作不参与事务(避免阻塞)。
2.2 传播机制(Propagation)
传播机制决定了一个事务方法调用另一个事务方法时,事务如何处理。
① REQUIRED(默认,最常用)
一句话: 有事务就加入,没事务就新建。
java
@Service
public class OrderService {
@Autowired
private PaymentService paymentService;
@Transactional(propagation = Propagation.REQUIRED)
public void createOrder(Order order) {
orderMapper.insert(order); // ①
paymentService.pay(order); // ② pay()也是 REQUIRED
}
}
@Service
public class PaymentService {
@Transactional(propagation = Propagation.REQUIRED)
public void pay(Order order) {
accountMapper.deduct(order.getUserId(), order.getAmount());
}
}执行流程:
createOrder() 被调用
→ 没有事务 → 新建事务 T1
→ 执行 orderMapper.insert() ← 在 T1 中
→ 调用 paymentService.pay()
→ 当前有事务 T1 → 加入 T1
→ 执行 accountMapper.deduct() ← 在 T1 中
→ pay() 返回
→ createOrder() 结束
→ T1 提交(insert 和 deduct 一起提交)
如果 deduct 抛异常 → T1 回滚(insert 和 deduct 都回滚)适用场景: 90% 的业务都用这个,一组操作要么全成功要么全失败。
② REQUIRES_NEW
一句话: 不管外面有没有事务,自己开一个新事务,挂起外面的事务。
java
@Service
public class OrderService {
@Autowired
private LogService logService;
@Transactional(propagation = Propagation.REQUIRED)
public void createOrder(Order order) {
orderMapper.insert(order); // 主事务
try {
logService.recordLog(order); // 独立事务
} catch (Exception e) {
log.error("日志记录失败,不影响订单", e);
}
}
}
@Service
public class LogService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void recordLog(Order order) {
logMapper.insert(order);
}
}执行流程(时间线):
createOrder 事务 T1:
┌──────────────────────────────────────────────────────────┐
│ ① insert 订单 (T1) │
│ ② 调用 recordLog() │
│ → T1 被挂起(暂停) │
│ ┌── 子事务 T2 ──────────────────────────┐ │
│ │ ③ insert 日志 (T2) │ │
│ │ ④ T2 提交(日志已持久化) │ │
│ └───────────────────────────────────────┘ │
│ → T1 恢复 │
│ ⑤ T1 提交(订单持久化) │
└──────────────────────────────────────────────────────────┘
关键:T2 提交后,即使 T1 回滚,日志也不会回滚!适用场景: 操作日志记录、发送通知/消息、审计记录(必须独立提交,不能被主事务回滚影响)。
③ NESTED
一句话: 有事务就在里面开一个嵌套事务(savepoint),没事务就和 REQUIRED 一样。
java
@Service
public class OrderService {
@Autowired
private LogService logService;
@Transactional(propagation = Propagation.REQUIRED)
public void createOrder(Order order) {
orderMapper.insert(order); // 主事务
try {
logService.recordLog(order); // 嵌套事务
} catch (Exception e) {
// 日志失败 → 回滚到 savepoint,主事务继续
}
// 主事务提交 → 日志也一起提交
}
}
@Service
public class LogService {
@Transactional(propagation = Propagation.NESTED)
public void recordLog(Order order) {
logMapper.insert(order);
}
}执行流程(savepoint 机制):
createOrder 事务 T1:
┌──────────────────────────────────────────────────────────┐
│ ① insert 订单 (T1) │
│ ② 创建 savepoint A │
│ ③ 调用 recordLog() │
│ → 日志在 T1 中执行(同一个连接,同一个事务) │
│ → 如果日志失败:ROLLBACK TO SAVEPOINT A │
│ → 日志回滚,订单还在,主事务继续 │
│ → 如果日志成功:继续 │
│ ④ T1 提交(订单 + 日志 一起提交) │
└──────────────────────────────────────────────────────────┘
关键:主事务回滚 → 日志也回滚(和 REQUIRES_NEW 不同!)NESTED vs REQUIRES_NEW 对比:
| 场景 | REQUIRES_NEW | NESTED |
|---|---|---|
| 子事务失败 | 主事务不受影响 | 主事务不受影响 |
| 主事务回滚 | 子事务不受影响 | 子事务也回滚 |
| 实现方式 | 独立数据库连接 | savepoint(同一连接) |
| 子事务提交时机 | 方法结束时立即提交 | 跟随主事务一起提交 |
| 事务关系 | T1 、T2分别独立 | T1和T2是嵌套事务 |
限制: 只有 JDBC 支持 savepoint,JPA 不支持 NESTED。
④ SUPPORTS
一句话: 有事务就加入,没有就裸跑(非事务执行)。
关键理解: SUPPORTS 的价值不在于查询本身,而在于一个方法被多种场景调用时自动适配。如果方法永远只被事务方法调用,用 REQUIRED;永远只被非事务方法调用,不需要注解。SUPPORTS 适用于「既可能被事务方法调用,也可能被非事务方法调用」的通用方法。
java
@Service
public class InventoryService {
@Transactional(propagation = Propagation.SUPPORTS)
public boolean checkStock(Long productId, int quantity) {
int stock = inventoryMapper.getStock(productId);
return stock >= quantity;
}
}
// 场景A:在事务中调用 → 加入事务,库存检查在事务中执行
@Transactional
public void createOrder(Order order) {
if (!inventoryService.checkStock(order.getProductId(), order.getQuantity())) {
throw new RuntimeException("库存不足");
}
orderMapper.insert(order); // 和 checkStock 在同一个事务中
}
// 场景B:非事务调用 → 正常执行,不需要事务
@GetMapping("/check/{productId}")
public ApiResult check(Long productId) {
// 用户只是查看库存,不需要事务
boolean available = inventoryService.checkStock(productId, 10);
return ApiResult.ok(available);
}场景A:调用方有事务
→ checkStock 加入事务,和主业务共享同一个连接/事务
场景B:调用方没有事务
→ checkStock 非事务执行,不占用事务资源⑤ NOT_SUPPORTED
一句话: 不管外面有没有事务,都以非事务方式运行。如果有事务,挂起它。
java
@Service
public class NotificationService {
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void sendSms(String phone, String message) {
// 调用第三方短信 API,耗时 3 秒
// 绝对不能放在事务中,否则事务会长时间持有数据库连接
smsClient.send(phone, message);
}
}执行流程:
createOrder 事务 T1:
→ ① insert 订单 (T1)
→ ② 调用 sendSms()
→ T1 被挂起,sendSms 非事务运行(不占用数据库连接)
→ T1 恢复
→ ③ T1 提交适用场景: 耗时的外部 API 调用、文件操作,不应在事务中执行。
NOT_SUPPORTED vs 没标注 @Transactional 的区别:
没有 @Transactional:
T1 调用 method() → method 在 T1 中执行(复用了事务连接)
NOT_SUPPORTED:
T1 调用 method() → T1 被挂起,method 非事务执行| 场景 | 无 @Transactional | NOT_SUPPORTED |
|---|---|---|
| 非事务方法调用 | 非事务执行 | 非事务执行 |
| 事务方法调用 | 复用事务连接(在 T1 中) | 挂起 T1,非事务执行 |
原理:
java
这就是为什么耗时操作要加 NOT_SUPPORTED: 不加注解的话,发短信 3 秒,Conn1 就白白被持有 3 秒,连接池可能被耗尽。@Transactional // 方法A:开启事务 T1,连接 Conn1 绑定到 ThreadLocal
public void methodA() {
orderMapper.insert(order); // 用 Conn1 执行
methodB(); // 调用 methodB
}
// 没有 @Transactional
public void methodB() {
logMapper.insert(log); // 还是 Conn1!被动在事务中
}
// NOT_SUPPORTED
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void methodC() {
smsClient.send(phone); // 挂起 T1,Conn1 解绑,不占连接
}⑥ MANDATORY
一句话: 必须要有事务,没有就抛异常。
java
@Service
public class InventoryService {
@Transactional(propagation = Propagation.MANDATORY)
public void deduct(Long productId, int quantity) {
inventoryMapper.deduct(productId, quantity);
}
}
// ❌ 直接调用 → 抛异常 IllegalTransactionStateException
inventoryService.deduct(1L, 5);
// ✅ 在事务中调用 → 正常
@Transactional
public void createOrder() {
inventoryService.deduct(1L, 5); // 正常执行
}适用场景: 强制要求调用方必须开启事务,起到"契约"作用。
⑦ NEVER
一句话: 必须不能有事务,有就抛异常。
java
@Service
public class ReportService {
@Transactional(propagation = Propagation.NEVER)
public void generateDailyReport() {
// 生成报表,数据量大,耗时久,明确禁止在事务中执行
List<Order> orders = orderMapper.findAll();
}
}
// ✅ 直接调用 → 正常
// ❌ 在事务中调用 → 抛异常 IllegalTransactionStateException总结对比
| 传播机制 | 外面有事务 | 外面没事务 | 使用频率 |
|---|---|---|---|
| REQUIRED | 加入 | 新建 | 极高 |
| REQUIRES_NEW | 新建独立事务,挂起外面 | 新建 | 高 |
| NESTED | 嵌套(savepoint) | 新建 | 中 |
| SUPPORTS | 加入 | 非事务 | 低 |
| NOT_SUPPORTED | 非事务,挂起外面 | 非事务 | 低 |
| MANDATORY | 加入 | 抛异常 | 低 |
| NEVER | 抛异常 | 非事务 | 极低 |
回滚关系详解(T1 = 外层事务,T2 = 内层事务)
| 传播机制 | T1 回滚 → T2? | T2 回滚 → T1? | 事务关系 |
|---|---|---|---|
| REQUIRED | 回滚 | 回滚 | 同一事务 |
| REQUIRES_NEW | 不影响 | 不影响(需 catch) | 独立事务 |
| NESTED | 回滚 | 不影响 | 嵌套事务(savepoint) |
| SUPPORTS(有事务) | 回滚 | 回滚 | 同一事务 |
| SUPPORTS(无事务) | — | 不影响 | 无事务 |
| NOT_SUPPORTED | 不影响 | 不影响 | 无事务 |
| MANDATORY | 回滚 | 回滚 | 同一事务 |
| NEVER | 不执行 | 不执行 | 无事务 |
REQUIRES_NEW 特别注意: T2 抛异常时,如果 T1 没有 catch,异常会传播到 T1 导致 T1 也回滚。所以需要 try-catch 才能真正做到互不影响。
T2 回滚,T1 catch 了异常:
T1: ┌── insert ──── T2 失败(catch) ──── 提交 ✅ ──┐
T2: └── insert 日志 ── 回滚 ❌ ──┘
T2 回滚,T1 没 catch:
T1: ┌── insert ──── T2 异常未捕获 ──── 回滚 ❌ ──┐
T2: └── insert 日志 ── 回滚 ❌ ──┘NESTED vs REQUIRES_NEW 关键区别:
| 场景 | REQUIRES_NEW | NESTED |
|---|---|---|
| T1 回滚 | T2 不受影响 | T2 也回滚 |
| T2 回滚(catch后) | T1 不受影响 | T1 不受影响 |
2.3 数据库并发问题与隔离级别
2.3.1 数据库三个并发问题
三个并发问题定义:
脏读:读到别人修改了但未提交的数据(对方回滚后,这些数据从未存在过)
不可重复读:同一事务内两次读同一行,结果不同(别人修改了并提交了)
幻读:同一事务内两次查同一条件,行数不同(别人插入了/删除了并提交了)速记区分:
脏读: 读一次就中招 → 读到未提交的脏数据
不可重复读: 读两次,值不同 → 别人 UPDATE 了同一行
幻读: 读两次,行数不同 → 别人 INSERT/DELETE 了同条件行| 维度 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| 现象 | 读一次 | 读两次,值不同 | 读两次,行数不同 |
| 对方操作 | 修改后未提交 | 修改后提交了 | 插入/删除后提交了 |
| 核心区别 | 读到未提交数据 | 同一行值变了 | 多了/少了行 |
2.3.2 数据库4种隔离级别详解
SQL 标准定义了 4 种隔离级别:
| 隔离级别 | 中文名字 |
|---|---|
| READ UNCOMMITTED | 读未提交 |
| READ COMMITTED | 读已提交 |
| REPEATABLE READ | 可重复读 |
| SERIALIZABLE | 串行化 |
核心理解:4种隔离级别逐级解决 3 种并发问题
READ UNCOMMITTED → 什么都不解决(性能最好,一致性最差)
↓
READ COMMITTED → 解决脏读
↓
REPEATABLE READ → 解决脏读 + 不可重复读
↓
SERIALIZABLE → 解决脏读 + 不可重复读 + 幻读(性能最差,一致性最强)| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 性能 |
|---|---|---|---|---|
| READ_UNCOMMITTED | 不解决 | 不解决 | 不解决 | 最高 |
| READ_COMMITTED | 解决 | 不解决 | 不解决 | 高 |
| REPEATABLE_READ | 解决 | 解决 | 不解决(MySQL 解决) | 中 |
| SERIALIZABLE | 解决 | 解决 | 解决 | 最低 |
口诀:
① READ_UNCOMMITTED(读未提交)
最低级别,允许脏读。
java
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public void transfer() {
// 事务 A 正在转账,还没提交
// 事务 B 能读到未提交的转账结果 → 脏读
// 如果事务 A 回滚了,事务 B 读到的就是脏数据
}时间线(脏读):
事务 A:UPDATE account SET balance = 500 WHERE id = 1 (未提交)
事务 B:SELECT balance FROM account WHERE id = 1 → 读到 500
事务 A:ROLLBACK → balance 回到 1000
事务 B:基于 500 做了后续操作 → 数据是错的!② READ_COMMITTED(读已提交)
只能读到已提交的数据,解决脏读。但存在不可重复读。
java
@Transactional(isolation = Isolation.READ_COMMITTED)
public void checkOrder() {
int count1 = mapper.count(); // 第一次读 → 10 条
// 此时另一个事务提交了插入操作
int count2 = mapper.count(); // 第二次读 → 11 条(不可重复读!)
// 同一事务内,两次读结果不同
}时间线(不可重复读):
事务 A:SELECT COUNT(*) FROM orders → 10
事务 B:INSERT INTO orders VALUES (...) → COMMIT
事务 A:SELECT COUNT(*) FROM orders → 11(同一事务,结果变了!)READ_COMMITTED 是 Oracle 默认级别,也是互联网公司最常用的级别之一。
③ REPEATABLE_READ(可重复读)
MySQL InnoDB 默认级别。通过 MVCC 保证同一事务内多次读同一行结果一致,解决不可重复读。
java
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void checkOrder() {
int count2024 = mapper.countByYear(2024); // 第一次读 → 10 条
// 此时另一个事务提交了插入(2024年的新订单)
int count2024 = mapper.countByYear(2024); // 第二次读 → 还是 10 条(一致!)
}时间线(可重复读,RL 解决不可重复读):
事务 A:SELECT * FROM orders WHERE id = 1 → name='张三'
事务 B:UPDATE orders SET name='李四' WHERE id = 1 → COMMIT
事务 A:SELECT * FROM orders WHERE id = 1 → name='张三'(同一行,一致!)但 MySQL 的 RR 下,幻读仍在某些场景存在:
sql
-- 事务 A
SELECT * FROM orders WHERE amount > 100; -- 3 条
-- 事务 B 插入了一条 amount=200 的数据并提交
SELECT * FROM orders WHERE amount > 100; -- 还是 3 条(快照读,没问题)
SELECT * FROM orders WHERE amount > 100 FOR UPDATE; -- 4 条!(当前读,幻读了)MySQL InnoDB 通过 Next-Key Lock(临键锁)解决了幻读: 在 RR 级别下,SELECT ... FOR UPDATE 会加间隙锁,阻止其他事务插入符合条件的行。
④ SERIALIZABLE(串行化)
最高级别,所有事务串行执行。读加共享锁,写加排他锁,完全杜绝并发问题,但性能最差。
java
@Transactional(isolation = Isolation.SERIALIZABLE)
public void transfer() {
// 事务 A 读 account 表 → 加共享锁,事务 B 不能写
// 事务 B 必须等事务 A 提交后才能操作
// 并发 → 串行,性能极低
}时间线(串行化):
事务 A:SELECT * FROM account WHERE id = 1 → 加共享锁
事务 B:UPDATE account SET balance = 500 WHERE id = 1 → 等待...
事务 A:COMMIT → 释放锁
事务 B:获得锁,执行 UPDATE适用场景: 金融系统、余额计算等对数据一致性要求极高的场景。
⑤ DEFAULT
java
@Transactional(isolation = Isolation.DEFAULT)
// 不指定,使用数据库默认。MySQL 默认 RR,Oracle 默认 RC2.3.3 隔离级别选择建议
MySQL 默认 RR,但互联网公司常用 RC,原因:
RC vs RR 的关键区别:
┌──────────────────────────────────────────────────────┐
│ RC:每次读取都生成新的 Read View(快照) │
│ → 能读到最新已提交的数据,但有不可重复读 │
│ → 间隙锁少,死锁概率低,并发性能好 │
│ │
│ RR:事务开始时生成一个 Read View,整个事务复用 │
│ → 同一事务内多次读结果一致(可重复读) │
│ → 间隙锁多,死锁概率高,并发性能差 │
└──────────────────────────────────────────────────────┘
为什么互联网公司用 RC:
· 高并发场景下 RC 间隙锁少,死锁大幅减少
· 不可重复读对业务影响小(大部分业务只关心数据是否已提交)
· 配合乐观锁(version 字段)可以替代 RR 的一致性保证三、TransactionInterceptor 源码解析
java
// 简化版 TransactionInterceptor 核心逻辑
public Object invoke(MethodInvocation invocation) throws Throwable {
// ① 获取事务属性(@Transactional 注解属性)
TransactionAttribute txAttr = getTransactionAttribute(method, targetClass);
// ② 获取事务管理器
PlatformTransactionManager tm = determineTransactionManager(txAttr);
// ③ 创建事务(根据传播机制决定是否新建)
TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, methodName);
Object retVal;
try {
// ④ 执行目标方法
retVal = invocation.proceed();
} catch (Throwable ex) {
// ⑤ 异常回滚
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
} finally {
// ⑥ 清理线程绑定的资源
cleanupTransactionInfo(txInfo);
}
// ⑦ 提交事务
commitTransactionAfterReturning(txInfo);
return retVal;
}各数据库隔离级别差异
SQL 标准定义了 4 种隔离级别,但各数据库支持情况不同:
| 隔离级别 | MySQL | Oracle | DB2 | PostgreSQL |
|---|---|---|---|---|
| READ UNCOMMITTED | 支持 | 不支持 | 支持(UR) | 支持(实际等同 RC) |
| READ COMMITTED | 支持 | 支持(默认) | 支持(CS,默认) | 支持(默认) |
| REPEATABLE READ | 支持(默认) | 不支持 | 支持(RS) | 支持(快照隔离) |
| SERIALIZABLE | 支持 | 支持 | 支持(RR) | 支持 |
关键差异:
Oracle 只有 2 种:RC + SERIALIZABLE
→ 没有 READ UNCOMMITTED 和 REPEATABLE READ
→ Oracle 的 SERIALIZABLE 也不是真正的串行化,而是基于快照实现
DB2 命名不同:UR = READ UNCOMMITTED, CS = READ COMMITTED, RS = REPEATABLE READ, RR = SERIALIZABLE
MySQL RR 特殊:通过 Next-Key Lock 解决了幻读,其他数据库的 RR 通常不能解决幻读
PostgreSQL:READ UNCOMMITTED 内部退化为 RC,REPEATABLE READ 基于快照Spring 配置注意事项:
java
// ❌ 在 Oracle 上使用 REPEATABLE READ 会报错
@Transactional(isolation = Isolation.REPEATABLE_READ) // Oracle 不支持!
public void createOrder() { ... }
// ✅ 建议:使用 DEFAULT,让数据库决定
@Transactional(isolation = Isolation.DEFAULT)
public void createOrder() { ... }
// ✅ 或者统一使用 RC(所有主流数据库都支持)
@Transactional(isolation = Isolation.READ_COMMITTED)
public void createOrder() { ... }实际开发建议: 多数团队统一使用 RC(READ COMMITTED),Oracle 和 PostgreSQL 默认就是 RC,MySQL 也建议改为 RC(RC 更接近 Oracle 行为,且能避免 RR 的间隙锁带来的死锁问题)。
四、事务失效的 8 个场景
核心原因:@Transactional 依赖 Spring AOP 代理,凡是让代理失效的因素,都会导致事务失效。
① 非 public 方法
原因: Spring AOP 默认基于 JDK 动态代理或 CGLIB,两者都只能代理 public 方法。非 public 方法上的 @Transactional 会被忽略。
java
@Service
public class OrderService {
@Transactional
void createOrder() { // ❌ 默认包可见,事务不生效
orderMapper.insert(order);
}
@Transactional
protected void updateOrder() { // ❌ protected,事务不生效
orderMapper.update(order);
}
@Transactional
private void deleteOrder() { // ❌ private,事务不生效
orderMapper.delete(id);
}
}
// ✅ 正确:使用 public
@Transactional
public void createOrder() {
orderMapper.insert(order);
}② 同类内部 this 调用
原因: 这是最常见的失效场景。this 调用的是原始对象,绕过了 Spring 的代理对象,AOP 拦截不到。
java
@Service
public class OrderService {
public void createOrder(Order order) {
orderMapper.insert(order); // 无事务
this.sendNotification(order); // ❌ this 调用,绕过代理
}
@Transactional
public void sendNotification(Order order) {
// 事务不生效!因为是通过 this 调用的,不是代理对象
notificationMapper.insert(order);
}
}解决方案:
java
// 方案1:依赖注入自己(推荐)
@Service
public class OrderService {
@Autowired
private OrderService self; // 注入代理对象
public void createOrder(Order order) {
orderMapper.insert(order);
self.sendNotification(order); // ✅ 通过代理调用
}
@Transactional
public void sendNotification(Order order) {
notificationMapper.insert(order);
}
}
// 方案2:拆到另一个 Service
@Service
public class OrderService {
@Autowired
private NotificationService notificationService;
public void createOrder(Order order) {
orderMapper.insert(order);
notificationService.sendNotification(order); // ✅ 跨类调用,走代理
}
}
// 方案3:AopContext.currentProxy()
@Service
public class OrderService {
public void createOrder(Order order) {
orderMapper.insert(order);
((OrderService) AopContext.currentProxy()).sendNotification(order); // ✅
}
}注意: 方案3 需要在启动类或配置类上加
@EnableAspectJAutoProxy(exposeProxy = true)
③ 异常被 catch 吞掉
原因: Spring 事务管理器通过捕获异常来决定回滚。如果你在方法内部 catch 了异常且没有重新抛出,Spring 感知不到异常,事务会正常提交。
java
@Transactional
public void createOrder(Order order) {
try {
orderMapper.insert(order);
int i = 1 / 0; // 抛异常
} catch (Exception e) {
log.error("创建订单失败", e);
// ❌ 异常被吞了,Spring 不知道,事务正常提交
}
// 此处事务会提交,insert 不会回滚
}解决方案:
java
// 方案1:catch 后手动回滚
@Transactional
public void createOrder(Order order) {
try {
orderMapper.insert(order);
int i = 1 / 0;
} catch (Exception e) {
log.error("创建订单失败", e);
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); // ✅ 手动回滚
throw e; // 或者重新抛出
}
}
// 方案2:不 catch 具体异常,让它自然抛出
@Transactional
public void createOrder(Order order) {
orderMapper.insert(order);
// 如果这里抛异常,Spring 自动回滚
}④ 抛出 checked 异常
原因: @Transactional 默认只回滚 RuntimeException 和 Error,对 checked 异常(如 Exception、IOException)不会回滚。
java
@Transactional
public void createOrder() throws Exception {
orderMapper.insert(order);
throw new Exception("业务异常"); // ❌ 默认不回滚
}解决方案:
java
// 指定 rollbackFor
@Transactional(rollbackFor = Exception.class) // ✅ 所有异常都回滚
public void createOrder() throws Exception {
orderMapper.insert(order);
throw new Exception("业务异常");
}
// 或在事务中手动回滚
@Transactional
public void createOrder() {
try {
orderMapper.insert(order);
throw new Exception("业务异常");
} catch (Exception e) {
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
throw new RuntimeException(e); // 包装成 RuntimeException
}
}⑤ 数据库引擎不支持事务
原因: MySQL 的 MyISAM 引擎不支持事务,@Transactional 在 MyISAM 表上无效,SQL 会直接提交。
sql
-- 查看表引擎
SHOW TABLE STATUS WHERE Name = 'orders';
-- 如果是 MyISAM,改为 InnoDB
ALTER TABLE orders ENGINE = InnoDB;java
@Transactional
public void createOrder() {
orderMapper.insert(order); // 如果表是 MyISAM → 直接持久化,不回滚
}MySQL 5.5+ 默认引擎是 InnoDB,一般不会遇到这个问题,但老项目需要注意。
⑥ 多线程调用
原因: Spring 事务通过 ThreadLocal 绑定数据库连接。子线程拿不到主线程的 ThreadLocal 中的连接,所以不在同一事务中。
java
@Transactional
public void createOrder(Order order) {
orderMapper.insert(order); // 主线程事务
new Thread(() -> {
// ❌ 子线程,不在同一事务中
logMapper.insert(order);
}).start();
// 主线程提交,子线程的操作不在事务中
}解决方案:
java
// 方案1:将子线程操作改为 REQUIRES_NEW 独立事务
@Transactional
public void createOrder(Order order) {
orderMapper.insert(order);
// 让子线程自己管理事务
CompletableFuture.runAsync(() -> {
logService.asyncRecordLog(order); // 独立事务
});
}
// 方案2:不用多线程,改为异步消息队列⑦ final 方法
原因: CGLIB 通过继承目标类来创建代理,final 方法不能被重写,所以 CGLIB 无法代理 final 方法。
java
@Service
public class OrderService {
@Transactional
public final void createOrder() { // ❌ final 方法,CGLIB 无法代理
orderMapper.insert(order);
}
}
// ✅ 去掉 final
@Transactional
public void createOrder() {
orderMapper.insert(order);
}注意: 如果使用 JDK 动态代理(基于接口),
final方法可以正常工作,因为 JDK 代理不继承类。但 Spring Boot 默认使用 CGLIB。
⑧ 注解放在抽象类/接口上
原因: 如果 @Transactional 放在接口或抽象类上,注解只在实现类通过代理调用时才生效,而且行为依赖代理模式。
java
// ❌ 放在接口上
public interface OrderService {
@Transactional
void createOrder(Order order);
}
@Service
public class OrderServiceImpl implements OrderService {
@Override
public void createOrder(Order order) {
// 只有 JDK 动态代理(基于接口)时事务才生效
// CGLIB 代理(默认)时可能不生效
}
}解决方案: 始终把 @Transactional 放在具体实现类的方法上,不要放在接口或抽象类上。
java
// ✅ 放在实现类上
@Service
public class OrderServiceImpl implements OrderService {
@Override
@Transactional
public void createOrder(Order order) {
orderMapper.insert(order);
}
}总结
| 场景 | 核心原因 | 解决 |
|---|---|---|
| 非 public 方法 | AOP 只能代理 public | 改为 public |
| this 内部调用 | 绕过代理对象 | 注入 self 或拆 Service |
| 异常被吞 | Spring 感知不到异常 | 手动回滚或重新抛出 |
| checked 异常 | 默认只回滚 RuntimeException | rollbackFor = Exception.class |
| MyISAM 引擎 | 引擎不支持事务 | 改为 InnoDB |
| 多线程调用 | ThreadLocal 隔离 | 子线程独立事务或 MQ |
| final 方法 | CGLIB 无法重写 | 去掉 final |
| 注解在接口上 | 代理模式不兼容 | 放在实现类上 |
五、编程式事务
详细内容请参见 编程式事务 独立文档。
六、面试要点
Q1:@Transactional 的原理是什么?
基于 AOP 动态代理。TransactionInterceptor 在方法执行前开启事务(关闭自动提交),执行目标方法,成功则提交,异常则回滚。整个过程中通过 ThreadLocal 绑定数据库连接,保证同一事务用同一连接。
Q2:@Transactional 失效的常见原因?
非 public 方法、同类 this 内部调用、异常被 catch 吞掉、抛出 checked 异常未指定 rollbackFor、数据库引擎不支持事务、final 方法。
Q3:REQUIRED 和 REQUIRES_NEW 的区别?
REQUIRED 加入已有事务,一起提交回滚;REQUIRES_NEW 挂起当前事务创建独立事务,提交回滚互不影响。典型场景:日志记录失败不影响主业务。
