Skip to content

声明式事务

  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_NEWNESTED
子事务失败主事务不受影响主事务不受影响
主事务回滚子事务不受影响子事务也回滚
实现方式独立数据库连接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 非事务执行
场景无 @TransactionalNOT_SUPPORTED
非事务方法调用非事务执行非事务执行
事务方法调用复用事务连接(在 T1 中)挂起 T1,非事务执行

原理:

java
@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 解绑,不占连接
}
这就是为什么耗时操作要加 NOT_SUPPORTED: 不加注解的话,发短信 3 秒,Conn1 就白白被持有 3 秒,连接池可能被耗尽。

⑥ 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_NEWNESTED
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 默认 RC

2.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 种隔离级别,但各数据库支持情况不同:

隔离级别MySQLOracleDB2PostgreSQL
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 默认只回滚 RuntimeExceptionError,对 checked 异常(如 ExceptionIOException)不会回滚。

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 异常默认只回滚 RuntimeExceptionrollbackFor = 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 挂起当前事务创建独立事务,提交回滚互不影响。典型场景:日志记录失败不影响主业务。