@Transactional 在微服务中失效了?Spring 事务 + Sentinel 兜底机制全解析

发布时间:2026/6/28 22:10:16
@Transactional 在微服务中失效了?Spring 事务 + Sentinel 兜底机制全解析 目录一、问题现象同样的代码单体能回滚微服务不行二、根本原因事务管理器的控制范围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) │ ├── 创建全局事务 XID12345 │ ├── Order 服务注册分支事务 branch_1 (XID12345) ├── Stock 服务注册分支事务 branch_2 (XID12345) ├── Account 服务注册分支事务 branch_3 (XID12345) │ ├── 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 规则拦截BlockExceptionfallback 处理的是业务异常。两者触发条件不同不能混用。误区三方法内部任意位置的异常都能被 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 只管当前服务用 GlobalTransactional2fallback 不触发异常被吞掉兜底方法没执行异常没有传播回被检测的方法不要 catch 了不抛3blockHandler 不生效限流了但返回了默认错误没有配 blockHandler 方法检查方法签名是否匹配4远程调用异常不走 blockHandler限流了但走的是 fallback从下游传回的异常不是 BlockException区分两种兜底的职责5Seata 回滚失败分支事务回滚报错undo_log 表不存在或数据被清理确保每个库都有 undo_log 表6SentinelResource 不生效限流规则配了但没拦截没有引入 Sentinel 依赖或注解拼写错误检查依赖和注解配置七、总结核心口诀事务跟着连接走单体一个微服务多个Sentinel 跟着声明走fallback 兜异常blockHandler 管规则。一句话速记Transactional 的边界 数据库连接的边界 单个服务的边界 SentinelResource 的边界 资源声明的边界 当前方法的边界 两者都是只管自己这一层不自动跨层传播。自检清单我能解释为什么 Transactional 在微服务中不能跨服务回滚我知道单体事务的底层是 ThreadLocal 单个 Connection我能说出 Seata AT 模式的回滚原理undo_log 反向补偿我能区分 blockHandler规则拦截和 fallback业务异常我知道 fallback 能兜住从下游传播回来的异常但 blockHandler 不行我知道异常被 catch 吞掉后 fallback 不会触发