目录

一、问题现象:同样的代码,单体能回滚,微服务不行

二、根本原因:事务管理器的控制范围

2.1 单体架构:一个事务管理器管所有

2.2 微服务架构:三个事务管理器各自为政

2.3 一句话对比

三、解决方案:Seata 全局事务

单体 vs 微服务 vs 微服务+Seata

四、进阶:Sentinel 的两种兜底机制

4.1 blockHandler vs fallback 的本质区别

4.2 代码示例

4.3 远程调用时,兜底机制还能生效吗?

4.4 关键理解

五、常见误区澄清

误区一:"微服务中 @Transactional 加了就能回滚全部"

误区二:"Sentinel 的 fallback 和 blockHandler 是一回事"

误区三:"方法内部任意位置的异常都能被 fallback 兜住"

误区四:"Seata 能保证强一致性"

六、避坑清单

七、总结

核心口诀

一句话速记

自检清单


🧩 一句话读懂:@Transactional 能保证单体服务内多表操作的一致性,但跨微服务后事务管理器各自为政,需要 Seata 全局事务;Sentinel 的 fallback 能兜住传播回来的异常,但 blockHandler 只管自己这一层。 🎯 适合人群:用过 @Transactional 和 Feign 远程调用,但对分布式事务和 Sentinel 兜底机制模糊的 Java 开发者 📊 难度等级:⭐⭐⭐⭐☆(进阶) ⏱ 阅读时长:约 15 分钟 💡 前置知识:Spring @Transactional、Feign 远程调用、Sentinel 基本概念


一、问题现象:同样的代码,单体能回滚,微服务不行

你可能写过这样的代码——在一个方法里操作三张表:

@Transactional(rollbackFor = Exception.class)
public Order createOrder(Long userId, Long productId, Integer num) {
    orderMapper.insert(order);           // 1. 插入订单
    stockMapper.deductStock(productId, num); // 2. 扣减库存
    accountMapper.deductBalance(userId, amount); // 3. 扣减余额
    // 如果余额不足抛异常,三张表全部回滚 ✅
}

单体服务下一切正常——余额不足时,订单和库存都会回滚。

但如果拆成微服务呢?

@Transactional(rollbackFor = Exception.class)
public Order createOrder(Long userId, Long productId, Integer num) {
    orderMapper.insert(order);                        // 本地操作
    stockFeignClient.deductStock(productId, num);     // 远程调用 Stock 服务
    accountFeignClient.deductBalance(userId, amount); // 远程调用 Account 服务
    // 余额不足抛异常 → 订单回滚了,但库存已经扣了 ❌
}

同样的 @Transactional,微服务下库存扣了不回滚。

这不是 Spring 的 Bug,而是两种架构的本质差异。


二、根本原因:事务管理器的控制范围

2.1 单体架构:一个事务管理器管所有

单体服务中,所有操作共享同一个数据库连接,由 Spring 的 PlatformTransactionManager 统一管理:

HTTP 请求进入
    │
    ├── Spring AOP 拦截 @Transactional
    │       ├─ 从连接池获取 Connection
    │       ├─ conn.setAutoCommit(false)  ← 开启事务
    │       └─ 绑定到 ThreadLocal
    │
    ├── orderMapper.insert(order)        ← 使用这个 Connection
    ├── stockMapper.deductStock(...)     ← 使用同一个 Connection
    ├── accountMapper.deductBalance(...) ← 使用同一个 Connection
    │       └─ 余额不足,抛异常!
    │
    └── Spring 捕获异常
            └─ conn.rollback()  ← 一个 rollback 撤销全部操作 ✅

底层原理简化:

// Spring 事务管理器的核心逻辑(简化版)
public void invokeWithinTransaction() {
    Connection conn = dataSource.getConnection();  // 获取一个连接
    conn.setAutoCommit(false);                     // 开启事务
    TransactionSynchronizationManager.bindResource(dataSource, conn); // 绑定到线程
​
    try {
        orderMapper.insert(order);           // → 从 ThreadLocal 取 conn
        stockMapper.deductStock(...);        // → 从 ThreadLocal 取同一个 conn
        accountMapper.deductBalance(...);    // → 从 ThreadLocal 取同一个 conn
        conn.commit();                       // 全部成功才提交
    } catch (Exception e) {
        conn.rollback();                     // 任何一个失败,全部回滚
    }
}

为什么能保证一致性?

关键点说明
同一个事务管理器Spring 的 PlatformTransactionManager 统一管理
同一个数据库连接从连接池获取一个 Connection,所有操作共用
同一个事务上下文通过 ThreadLocal 绑定当前线程的事务信息
原子性保证ACID 中的 Atomicity 由数据库保证

2.2 微服务架构:三个事务管理器各自为政

拆成微服务后,每个服务有独立的 JVM、独立的数据库连接、独立的事务管理器:

Order 服务 (进程A)              Stock 服务 (进程B)           Account 服务 (进程C)
    │                               │                            │
    ├── 连接 OrderDB               │                            │
    ├── BEGIN TX_A                 │                            │
    ├── INSERT t_order             │                            │
    │                               │                            │
    ├── HTTP POST ────────────────>│                            │
    │                               ├── 连接 StockDB             │
    │                               ├── BEGIN TX_B              │
    │                               ├── UPDATE t_stock          │
    │                               ├── COMMIT TX_B ✅ (已提交) │
    │                               │                            │
    ├── HTTP POST ──────────────────────────────────────────────>│
    │                               │                            ├── 连接 AccountDB
    │                               │                            ├── BEGIN TX_C
    │                               │                            ├── 检查余额不足!
    │                               │                            ├── ROLLBACK TX_C
    │                               │                            │
    ├── 捕获异常                   │                            │
    ├── ROLLBACK TX_A ❌           │                            │
    │                               │                            │
结果:TX_A 回滚了,TX_C 回滚了,但 TX_B 已经提交!库存扣了但订单没创建 → 数据不一致 ❌

为什么不能回滚?

关键问题说明
不同的事务管理器三个服务各自有独立的 PlatformTransactionManager
不同的数据库连接OrderDB、StockDB、AccountDB 是完全独立的数据库
不同的进程空间三个 JVM,三个线程,没有任何共享上下文
HTTP 协议无事务HTTP 请求不支持事务传播,Spring 无法跨越 HTTP 管理事务

2.3 一句话对比

单体:一个事务管理器 + 一个 Connection = ACID 完整保证
微服务:三个事务管理器 + 三个 Connection = 没有"超级管理员"来协调

类比:单体就像在同一家银行里转账——系统保证原子性。微服务就像跨三家银行转账——没有统一的清算中心,A 银行扣了钱,B 银行不一定能加上。


三、解决方案:Seata 全局事务

Seata 的作用就是充当那个"超级管理员":

@GlobalTransactional  // ← Seata 的全局事务注解,替代 @Transactional
public Order createOrder(...) {
    orderMapper.insert(order);
    stockFeignClient.deductStock(...);
    accountFeignClient.deductBalance(...);
    // 任何一个服务失败 → 全部回滚 ✅
}

Seata 的工作原理:

Seata Transaction Manager (TM)
     │
     ├── 创建全局事务 XID=12345
     │
     ├── Order 服务:注册分支事务 branch_1 (XID=12345)
     ├── Stock 服务:注册分支事务 branch_2 (XID=12345)
     ├── Account 服务:注册分支事务 branch_3 (XID=12345)
     │
     ├── Account 服务失败,上报给 TM
     │
     ├── TM 发起全局回滚:
     │    ├─ 通知 branch_1 回滚 ✅ (通过 undo_log 反向补偿)
     │    ├─ 通知 branch_2 回滚 ✅ (通过 undo_log 反向补偿)
     │    └─ 通知 branch_3 回滚 ✅ (本地已回滚)
     │
所有服务的数据都回滚,保持一致性 ✅

核心机制:每个分支事务在执行时会写一条 undo_log(回滚日志),全局回滚时 Seata 读取 undo_log 执行反向 SQL 来补偿。

单体 vs 微服务 vs 微服务+Seata

维度单体架构微服务架构微服务 + Seata
事务管理器1 个N 个,各自独立1 个全局 TM + N 个本地 TM
数据库连接1 个N 个N 个
能否自动回滚全部✅ 能❌ 不能✅ 能
事务传播方式ThreadLocal❌ 无法传播XID 通过 HTTP Header 传递
回滚机制conn.rollback()各自独立Undo Log 反向补偿
一致性保证ACID(强一致)无保证最终一致性

四、进阶:Sentinel 的两种兜底机制

理解了事务的边界后,再来看 Sentinel 的兜底机制——它和事务一样,保护范围跟着声明走,不会自动跨层传播。

4.1 blockHandler vs fallback 的本质区别

维度blockHandlerfallback
处理什么Sentinel 规则拦截(流控/熔断/热点/系统规则)业务异常(所有 Throwable)
触发条件触发了 Sentinel 配置的规则方法内部抛出了未捕获的异常
异常类型BlockException 的子类任意 Throwable
适用场景限流降级、熔断降级业务逻辑兜底、远程调用失败

4.2 代码示例

@SentinelResource(
    value = "create-order",
    blockHandler = "createOrderBlockHandler",  // 规则拦截时走这里
    fallback = "createOrderFallback"           // 业务异常时走这里
)
public String createOrder(String userId) {
    // 业务逻辑
    if (userId.equals("blacklist")) {
        throw new RuntimeException("黑名单用户");  // → fallback
    }
    return stockFeignClient.deductStock(userId);  // 远程调用
}

// 规则拦截兜底(限流、熔断等)
public String createOrderBlockHandler(String userId, BlockException ex) {
    return "系统繁忙,请稍后重试(规则拦截:" + ex.getRule() + ")";
}

// 业务异常兜底
public String createOrderFallback(String userId, Throwable ex) {
    return "下单失败,请稍后重试(异常:" + ex.getMessage() + ")";
}

执行流程:

请求进入
    │
    ├── Sentinel 检查规则
    │    ├─ QPS > 阈值?→ 触发流控 → blockHandler 兜底
    │    ├─ 慢调用比例 > 50%?→ 触发熔断 → blockHandler 兜底
    │    └─ 规则通过 ↓
    │
    ├── 执行 createOrder 方法
    │    ├─ 抛出业务异常 → fallback 兜底
    │    ├─ 远程调用超时 → fallback 兜底
    │    └─ 正常返回 ✅

4.3 远程调用时,兜底机制还能生效吗?

能,但有条件。

以 stockFeignClient.deductStock() 为例,调用链是:

createOrder (Sentinel 检测的方法)
    └── stockFeignClient.deductStock() (Feign 远程调用)
            └── Stock 服务处理

情况一:远程调用抛异常(超时、连接失败)

stockFeignClient.deductStock() 抛出异常
    → 异常沿调用链传播回 createOrder
    → fallback 兜住 ✅

情况二:远程调用被 Sentinel 限流

Stock 服务自己配了 @SentinelResource
    → Stock 服务抛出 BlockException
    → 异常传播回 createOrder
    → fallback 兜住 ✅(不是 blockHandler,因为异常是从下游传回来的)

情况三:想让 blockHandler 也保护远程调用

需要在 Feign 客户端调用处也声明 Sentinel 资源:
    → 用 @SentinelResource 标注 Feign 接口
    → 或用 Sentinel 的 Feign 拦截器自动适配

4.4 关键理解

Sentinel 的兜底机制是跟着资源声明走的,不是跟着调用链自动传播的。

  • fallback 能兜住从下游传播回来的异常

  • blockHandler 只管自己这一层的规则拦截

  • 远程模块需要自己配 Sentinel 才能被保护


五、常见误区澄清

误区一:"微服务中 @Transactional 加了就能回滚全部"

❌ 错。 @Transactional 只能管理当前服务的数据库连接,无法跨越 HTTP 调用其他服务的事务。

误区二:"Sentinel 的 fallback 和 blockHandler 是一回事"

❌ 错。 blockHandler 处理的是 Sentinel 规则拦截(BlockException),fallback 处理的是业务异常。两者触发条件不同,不能混用。

误区三:"方法内部任意位置的异常都能被 fallback 兜住"

⚠️ 不完全对。 异常必须未被捕获且传播回 @SentinelResource 标注的方法。如果异常被 try-catch 吞掉了,fallback 不会触发。

// ❌ 异常被吞掉,fallback 不会触发
@SentinelResource(fallback = "fallback")
public String createOrder() {
    try {
        int i = 5 / 0;
    } catch (Exception e) {
        log.error("出错了", e);  // 吞掉了,没有抛出去
    }
    return "success";  // 正常返回
}

误区四:"Seata 能保证强一致性"

⚠️ 不完全对。 Seata 的 AT 模式保证的是最终一致性,不是强一致性。在全局事务提交到各分支回滚完成之间有一个短暂的窗口期,数据是不一致的。如果需要强一致性,需要用 Seata 的 TCC 模式或 XA 模式。


六、避坑清单

#坑点现象原因✅ 避坑方案
1微服务中 @Transactional 不回滚库存扣了但订单没创建@Transactional 只管当前服务用 @GlobalTransactional
2fallback 不触发异常被吞掉,兜底方法没执行异常没有传播回被检测的方法不要 catch 了不抛
3blockHandler 不生效限流了但返回了默认错误没有配 blockHandler 方法检查方法签名是否匹配
4远程调用异常不走 blockHandler限流了但走的是 fallback从下游传回的异常不是 BlockException区分两种兜底的职责
5Seata 回滚失败分支事务回滚报错undo_log 表不存在或数据被清理确保每个库都有 undo_log 表
6@SentinelResource 不生效限流规则配了但没拦截没有引入 Sentinel 依赖或注解拼写错误检查依赖和注解配置

七、总结

核心口诀

事务跟着连接走,单体一个微服务多个;Sentinel 跟着声明走,fallback 兜异常,blockHandler 管规则。

一句话速记

@Transactional 的边界 = 数据库连接的边界 = 单个服务的边界
@SentinelResource 的边界 = 资源声明的边界 = 当前方法的边界
两者都是"只管自己这一层",不自动跨层传播。

自检清单

  • 我能解释为什么 @Transactional 在微服务中不能跨服务回滚
  • 我知道单体事务的底层是 ThreadLocal + 单个 Connection
  • 我能说出 Seata AT 模式的回滚原理(undo_log 反向补偿)
  • 我能区分 blockHandler(规则拦截)和 fallback(业务异常)
  • 我知道 fallback 能兜住从下游传播回来的异常,但 blockHandler 不行
  • 我知道异常被 catch 吞掉后 fallback 不会触发

更多推荐