分布式事务选型决策树与反模式

2 阅读58分钟

概述

系列定位:本文是“分布式事务工程实践”系列的第八篇暨收官之作。在前7篇中,我们逐篇深入拆解了XA/JTA(第1篇)、Seata AT(第2篇)、TCC(第3篇)、Saga(第4篇)、可靠消息最终一致性(第5篇)和CDC发件箱(第6篇)的原理、实现与源码细节,第7篇构建了生产级分布式事务监控与手动恢复平台。本文在此基础之上,帮助读者完成从“知道每种方案”到“能根据业务选型”再到“能避开常见坑”的能力闭环。选型决策树是系列知识的综合应用,反模式案例是前7篇理论在生产环境中的典型错误总结。

引言:分布式事务有六种主流方案,每种方案都有其最佳场景和致命弱点。XA强一致但TPS不到500,AT平衡了性能与侵入性但存在脏读窗口,TCC性能最高但需要手写防悬挂和幂等,Saga适合长事务但补偿失败需人工介入,消息最简单但只适用于异步场景。选错了方案,轻则性能瓶颈,重则数据永久不一致。本文首先通过10个维度的量化对比矩阵,将六种方案放在同一坐标系中比较——TPS、延迟、锁持有时间、侵入性、运维复杂度一目了然。然后通过七步决策树,从事务跨度、一致性要求、TPS、侵入接受度、资源预留、Kafka生态、基础设施七个维度逐步收敛,每步给出典型业务场景。最后通过九大反模式案例——从XA的In-Doubt Transaction阻塞到CDC的Binlog格式错误,每个案例严格遵循六步排查法,从错误示例到修正方案,并关联第7篇的监控指标与手动恢复操作。当你在架构评审中争论“该用AT还是TCC”,当生产环境出现悬挂事务告警需要快速定位,当Debezium Connector状态变红需要紧急修复——本文提供的决策框架和反模式排查指南将是直接的工程参考。

核心要点

  • 量化对比矩阵:10维度×6方案,含TPS/延迟/锁持有/侵入性/运维复杂度/回滚机制的量化数据与数据来源。
  • 七步决策树:事务跨度→一致性→TPS→侵入性→资源预留→Kafka生态→基础设施,每步含典型场景。
  • 九大反模式:XA(1个)、AT(2个)、TCC(3个)、Saga(1个)、消息(1个)、CDC(1个),每个六步排查法。
  • 监控联动:每个反模式关联第7篇的监控指标与Grafana面板。
  • 跨系列关联:每个反模式显式标注对应前文篇号。

文章组织架构图

flowchart LR
    subgraph 选型决策层
        A[1. 六大方案量化对比矩阵]
        B[2. 七步选型决策树]
    end
    subgraph 反模式排查层
        C[3. XA反模式: In-Doubt阻塞]
        D[4. AT反模式: 脏读窗口 & undo_log膨胀]
        E[5. TCC反模式: 悬挂/空回滚/幂等缺失]
        F[6. Saga反模式: 补偿失败无人工介入]
        G[7. 消息反模式: outbox SENDING残留]
        H[8. CDC反模式: Binlog格式错误]
    end
    subgraph 监控联动层
        I[9. 反模式与监控指标联动]
    end
    subgraph 总结巩固层
        J[10. 面试高频专题]
        K[速查表: 选型速查表 & 反模式排查速查表]
    end
    A --> B
    B --> C
    B --> D
    B --> E
    B --> F
    B --> G
    B --> H
    C --> I
    D --> I
    E --> I
    F --> I
    G --> I
    H --> I
    I --> J
    J --> K

架构图说明

  • 总览:全文10个模块从宏观的对比矩阵与决策树出发,逐步深入到各方案的反模式案例,最后以监控联动和面试题收尾,形成从选型→开发→监控→排障→恢复的完整闭环。
  • 逐模块说明:模块1-2是选型决策层,建立全局视野;模块3-8是反模式排查层,覆盖六种方案的九大经典错误,每个案例均采用六步排查法并关联前文源码;模块9是监控联动层,将反模式与第7篇的Prometheus指标、告警级别、Grafana面板串联;模块10和速查表提供面试巩固与快速参考。
  • 关键结论:分布式事务选型的核心是在一致性、性能、侵入性三者之间找到业务可接受的平衡点。反模式本质上是理论边界在工程中的体现——理解每种方案的安全模型与故障模式,是正确选型与高效排障的前提。

1. 六大方案量化对比矩阵

为了在同一尺度下评估XA、AT、TCC、Saga、可靠消息最终一致性(本地消息表/事务消息)、CDC发件箱六种方案,我们从10个维度进行量化对比。数据基于业界公开基准测试与系列前文的理论分析,假设环境为:4核8GB MySQL 8.0,千兆网络,单表CRUD业务,Seata 1.6.x(AT模式),RocketMQ 5.x,Debezium 2.6.x,Kafka 3.x,Atomikos 4.x。TPS上限为方案本身引入的额外开销限制下的业务TPS,非极限压测值。

维度XA (2PC)Seata ATTCCSaga可靠消息 (本地消息表/事务消息)CDC 发件箱
一致性强度强一致(ACID)强一致(读已提交+全局锁隔离)强一致(业务层资源预留)最终一致(正向+补偿)最终一致(异步投递)最终一致(异步投递)
TPS上限~5002000-30003000-50005000+10000+10000+
延迟(事务额外开销)+10-50ms(2次网络往返+磁盘fsync)+5-20ms(undo_log INSERT)+<5ms(Try本地事务提交)分钟级(事务跨度)1-3s(本地消息表扫描)/<100ms(RocketMQ事务消息)<100ms(Binlog采集+投递)
锁持有时间全局全程(Prepare至Commit/Rollback)本地锁到Phase1结束,全局锁到Phase2Try阶段结束(业务资源预留释放)无全局锁无锁无锁
业务侵入性零侵入(数据源代理)零侵入(数据源代理)高侵入(手写Try/Confirm/Cancel)中侵入(手写Ti/Ci)低侵入(需额外发件箱表)低侵入(CDC透明采集)
运维复杂度中(需协调者HA,JDBC驱动支持)中(Seata Server集群)中(需维护业务悬挂/幂等表)高(补偿链编排、人工恢复平台)低(本地消息表无外部依赖)高(Kafka+Debezium+MySQL Binlog管理)
回滚机制数据库自动回滚(UNDO日志)自动逆向SQL(undo_log)手动Cancel补偿手动Ci逆序补偿无需回滚(正向消息)无需回滚
适用事务跨度秒级(刚性事务)秒级(刚性)秒级分钟~小时级(长事务)异步(无同步返回)异步
故障恢复协调者恢复自动Commit/Rollback,或手动xa commit/rollbackTC重试Phase2,undo_log自动清理框架重试Confirm/Cancel,或手动防悬挂恢复重试补偿,失败需人工介入死信重投,SENDING超时回退Connector自动重启,Binlog offset恢复
社区生态与学习成本成熟标准,学习成本低阿里开源,Spring Cloud Alibaba生态需理解资源预留模型需编排框架(如Camunda)简单,无特殊依赖Debezium+Kafka Connect,学习成本高

量化数据详解

  • XA TPS ~500:主要受2PC协调者单点瓶颈及全程锁持有的影响。在“4C8G MySQL + 千兆网络”环境中,单个全局事务至少经历2次网络往返(Prepare+Commit),期间数据库持锁,导致并发度极低。实测100并发线程下,XA模式下单表UPDATE场景TPS约480-520(参见第1篇模块4性能分析)。
  • AT TPS 2000-3000:Seata AT在Phase1结束后即释放本地锁,并发度大幅提升。但每个分支事务需额外写入undo_log并注册全局锁,额外写入开销约5-20ms。在同样环境下,200并发线程下,Seata AT可稳定达到2000-3000 TPS(第2篇模块6压测数据)。
  • TCC TPS 3000-5000:TCC无全局锁,Try阶段即提交本地事务,性能接近常规单库事务。但业务需额外处理幂等和悬挂,实际TPS受业务逻辑复杂度影响。典型金融场景(冻结-扣款)下,TPS可达3000-5000(第3篇模块4基准测试)。
  • Saga TPS 5000+:Saga完全无锁,每步正向事务独立提交,吞吐量取决于步骤间消息传递效率。在事件驱动架构中,Saga编排的TPS可轻松超过5000,但整体事务完成时间受限于步骤序列。
  • 消息/CDC TPS 10000+:异步消息方案仅需本地事务+消息发送,无额外协调开销。在4C8G MySQL+RocketMQ环境下,以发件箱模式实现的可靠消息TPS可达12000+(第5篇模块3)。CDC发件箱同样高性能,延迟低于100ms(第6篇模块2)。
  • 延迟定义:XA延迟指从开启全局事务到提交完成相对于本地事务的额外时间;AT为undo_log写入+注册分支的额外时间;TCC为Try额外消耗;Saga延迟为整个事务从开始到所有步骤完成的时间跨度;消息方案延迟为消息从生产到消费的平均时间。

六大方案核心维度雷达图(一致性、吞吐量、低延迟、无锁、非侵入、运维简单,数值越高越好,其中“低延迟”指事务引入的额外延迟越低越好,Saga因长事务得分较低):

radar
    title 六大方案核心维度对比
    axis Consistency, Throughput, LowLatency, LockFree, NonInvasive, OpsSimple
    axis min 0
    axis max 5
    series XA    [5, 1, 2, 1, 5, 3]
    series AT    [5, 3, 4, 2, 5, 3]
    series TCC   [5, 4, 5, 3, 2, 3]
    series Saga  [3, 5, 2, 5, 3, 1]
    series Msg   [3, 5, 4, 5, 4, 5]
    series CDC   [3, 5, 5, 5, 4, 2]

图1说明

  • 总览:雷达图将六种方案在一致性、吞吐量、低延迟、锁持有、非侵入性、运维简单六个维度上的相对优势可视化。
  • 逐维度解读
    • 一致性:XA/AT/TCC均为5(强一致),Saga/消息/CDC为3(最终一致),因存在短暂不一致窗口。
    • 吞吐量:消息/CDC/Saga为5(最高),TCC为4,AT为3,XA仅为1。
    • 低延迟:TCC和CDC为5(额外开销极低);AT为4;消息方案因扫描间隔或RocketMQ半消息机制为4;XA为2(两次网络往返);Saga为2(事务跨度长,非单次延迟)。
    • 无锁:Saga/消息/CDC为5(无全局锁);TCC为3(资源预留结束后释放);AT为2(全局锁到Phase2);XA为1(全程锁)。
    • 非侵入:XA/AT为5(透明代理);消息/CDC为4(需发件箱表或CDC配置);Saga为3(手写正向/补偿逻辑);TCC为2(手写三接口+防悬挂)。
    • 运维简单:消息方案为5(无外部中间件依赖,若用本地消息表);XA/AT/TCC为3(需维护协调者/TC集群,TCC还需维护防悬挂表);CDC为2(需管理Kafka、Debezium、Binlog);Saga为1(需编排框架、补偿失败人工处理)。
  • 关键结论:没有银弹,性能与一致性正相关,侵入性与灵活性负相关。架构师需在具体业务目标下寻找帕累托最优。

2. 七步选型决策树

以下决策树帮助读者根据业务需求逐步收敛到最合适的分布式事务方案。每步均附典型场景及关联前文分析。

flowchart TD
    Start([开始选型]) --> S1{Step1: 事务跨度?}
    S1 -->|秒级同步| S2{Step2: 一致性要求?}
    S1 -->|分钟级/长事务| S_Saga[Saga 长事务]
    S1 -->|异步通知| S6{Step6: 是否有Kafka生态?}
    
    S2 -->|强一致| S3{Step3: TPS上限?}
    S2 -->|最终一致| S5{Step5: 是否有资源预留概念?}
    
    S3 -->|<500| S_XA[XA/2PC]
    S3 -->|500-3000| S_AT[Seata AT]
    S3 -->|3000-5000| S4{Step4: 业务侵入接受度?}
    S3 -->|>5000| S4
    
    S4 -->|可接受侵入| S_TCC[TCC]
    S4 -->|零侵入| S_AT_opt[AT + 读写分离/缓存]
    
    S5 -->|有冻结/预留概念| S_TCC
    S5 -->|无| S_Saga
    
    S6 -->|有Kafka| S_CDC[CDC发件箱]
    S6 -->|无Kafka| S7{Step7: 是否已有RocketMQ?}
    S7 -->|是| S_RocketMQ[RocketMQ事务消息]
    S7 -->|否| S_LocalMsg[本地消息表]
    
    S_XA --> Infra{Step7: 基础设施评估?}
    S_AT --> Infra
    S_TCC --> Infra
    S_Saga --> Infra
    S_RocketMQ --> Infra
    S_LocalMsg --> Infra
    S_CDC --> Infra
    
    Infra -->|已有Seata Server| ReuseSeata[复用Seata AT/TCC]
    Infra -->|已有RocketMQ| ReuseRocketMQ[复用RocketMQ事务消息]
    Infra -->|无基础设施| ZeroDep[本地消息表/自建CDC]

图2说明

  • 总览:决策树从事务跨度出发,通过七个决策点逐步将六种方案收敛至最适合的一种,每条路径均对应真实业务场景。
  • 逐步解析与典型场景
    • Step1 事务跨度:如果业务需要同步返回结果且耗时在秒级,走刚性事务分支(XA/AT/TCC)或同步消息;若允许分钟级不一致(如跨国汇款,涉及多个银行系统,耗时5-30分钟),直接选Saga;若仅需异步通知(如订单支付成功后通知物流),走向消息方案。数据来源:第4篇Saga长事务模型、第5篇异步消息模型。
    • Step2 一致性:强一致场景(资金转账、库存扣减)必须在XA/AT/TCC中选;最终一致场景(积分累积、邮件发送)可放宽至Saga或消息。一致性强度定义详见第1篇(ACID与BASE)。
    • Step3 TPS:<500且需强一致,XA可胜任;500-3000,AT是较好平衡,兼顾零侵入与性能;3000-5000,TCC释放锁优势明显;>5000,必须考虑Saga或异步消息。若TPS>5000仍需强一致,可考虑TCC+限流或基于业务的最终一致性补偿(如对账)。TPS数据基于第2篇模块6锁持有分析。
    • Step4 侵入性:如果团队不愿改造业务代码,优先XA/AT;若可接受,TCC能提供更高并发。注意,AT虽零侵入,但存在脏读窗口,可能需对部分查询加@GlobalLock(轻度侵入)。来源:第2篇模块5@GlobalLock机制。
    • Step5 资源预留:业务有“冻结”概念(如冻结库存、冻结资金),TCC的Try阶段天然匹配;若无预留概念(如机票座位无法冻结,只能直接扣减),Saga更合适。来源:第3篇TCC Try-Confirm-Cancel模型。
    • Step6 Kafka生态:已有Kafka,推荐CDC发件箱,延迟<100ms,且与业务解耦;无Kafka,但有RocketMQ,用事务消息;都没有,使用最简单的本地消息表。来源:第6篇CDC发件箱、第5篇本地消息表与RocketMQ事务消息。
    • Step7 基础设施:已部署Seata Server,优先复用AT或TCC,降低运维成本;已有RocketMQ,优先事务消息;全无,本地消息表零依赖起步,后续可演进到CDC。
  • 完整决策路径示例
    • 订单状态流转:异步 + 最终一致 + 高TPS + 零侵入 + 无Kafka → 本地消息表(详见第5篇)。
    • 资金转账:秒级 + 强一致 + TPS 3000 + 可侵入 + 有冻结概念 → TCC(详见第3篇)。
    • 跨国汇款:分钟级长事务 + 最终一致 + TPS不限 → Saga(详见第4篇)。
    • 秒杀库存扣减:秒级 + 强一致 + TPS 8000(超高峰期)→ TCC(冻结库存)或 Saga+缓存异步扣减。决策路径:秒级→强一致→>5000→可侵入(TCC)或零侵入(AT不可行,选Saga异步+最终一致可忍?需产品确认)。
  • 数据来源:TPS瓶颈分析详见第2篇模块6锁持有分析;CDC延迟对比见第6篇模块1。

3. XA反模式:In-Doubt Transaction阻塞(关联第1篇)

错误示例

某系统使用Atomikos作为JTA协调者,配置了两个MySQL资源参与2PC。未设置transaction-timeout,协调者单点部署。某次协调者JVM崩溃,一个全局事务的Prepare阶段已成功,但未完成Commit。

<!-- Atomikos未配置超时,无限等待 -->
<property name="com.atomikos.icatch.default_jta_timeout" value="0"/> 
<!-- 协调者单点,无HA -->

现象描述

数据库其他业务连接长时间等待锁,错误日志出现Lock wait timeout exceeded; try restarting transactionSHOW ENGINE INNODB STATUS显示大量锁等待,事务处于PREPARED状态已超过10分钟。业务表现为特定商品库存扣减操作挂起,前台超时。

排查思路

  1. 登录MySQL,执行xa recover;,列出所有Prepared但未完成的事务分支,输出格式如:
    mysql> xa recover;
    +----------+--------------+--------------+-----------------------+
    | formatID | gtrid_length | bqual_length | data                  |
    +----------+--------------+--------------+-----------------------+
    |        1 |           20 |           10 | 0x78696431...         |
    +----------+--------------+--------------+-----------------------+
    
    其中data字段包含xid。
  2. 检查事务持有锁情况SELECT * FROM information_schema.innodb_trx WHERE trx_state = 'PREPARED'; 可查看到该事务已运行时间,正在持有的行锁。
  3. 检查协调者事务日志:如Atomikos将事务日志存储在数据库表tm_log中,查询状态为PREPAREDcoordinator_id对应已宕机实例的记录。
  4. 关联第7篇监控:Grafana L1面板显示xa_indoubt_transactions_total数值为2(两个分支),触发P0告警。

根因分析

2PC协议中,协调者在发出Commit决策前宕机,资源管理器(MySQL)上的事务分支处于Prepared状态并一直持有锁(详见第1篇模块3源码分析XAResource.prepare()方法,调用后资源管理器将事务状态设为Prepared,锁在此时已获取)。由于协调者未恢复,该In-Doubt事务一直未决,阻塞所有后续事务对相同行的更新。MySQL的innodb_lock_wait_timeout默认50秒,超时后客户端报错。

修正方案

  1. 手动处理In-Doubt事务:根据xa recover得到的xid(假设为X'78696431...'),确认业务能提交则执行xa commit X'78696431...',不能确定则执行xa rollback X'78696431...'释放锁。
  2. 重启协调者:若协调者能恢复事务日志,它会尝试重新完成Commit/Rollback。
  3. 配置超时:设置default_jta_timeout=300000(5分钟),防止永久锁持有。
  4. 协调者高可用:使用Atomikos ExtremeTransactions或基于共享数据库的TM日志,实现多实例failover。
  5. 一键恢复:通过第7篇手动恢复平台,查询In-Doubt事务列表,提供“强制提交”或“强制回滚”按钮。

最佳实践

  • 必须设置transaction-timeout,宁可业务失败也不可无限期阻塞。
  • 协调者集群化,事务日志持久化到共享存储或数据库。
  • 监控xa_indoubt_transactions_total,P0告警:值>0立即通知DBA。
  • 定期演练协调者宕机后的手动恢复流程,形成应急预案。
  • 生产环境应优先评估是否可避免使用XA,选择对锁持有时间更短的方案。

排查时序图

sequenceDiagram
    participant App as 应用
    participant TC as 协调者(Atomikos)
    participant DB1 as MySQL分支1
    participant DB2 as MySQL分支2
    App->>TC: begin()
    TC->>DB1: prepare xid1
    DB1-->>TC: OK (Prepared, 持锁)
    TC->>DB2: prepare xid2
    DB2-->>TC: OK (Prepared, 持锁)
    Note over TC: 协调者宕机!
    App->>DB1: update同记录(等待锁)
    DB1-->>App: 锁超时 (Lock wait timeout)
    Note over DB1,DB2: DBA通过监控发现In-Doubt=2
    DBA->>DB1: xa recover
    DB1-->>DBA: xid1,xid2列表
    DBA->>DB1: xa commit 'xid1'
    DBA->>DB2: xa commit 'xid2'
    DB1-->>DBA: OK
    DB2-->>DBA: OK

图3说明

  • 总览:该时序图描述了协调者宕机导致In-Doubt Transaction阻塞的完整过程,以及手动恢复步骤。
  • 关键阶段:prepare成功后持有锁,协调者宕机后锁未释放,直到手动xa commit/rollback
  • 监控关联:此时xa_indoubt_transactions_total指标为2,触发P0告警,DBA通过第7篇Grafana L1面板确认In-Doubt数量,再进入L3单事务追踪定位xid。
  • 恢复路径:手动执行SQL或通过Admin后台一键恢复。

4. AT反模式:脏读窗口未加 @GlobalLock + undo_log表膨胀(关联第2篇)

4.1 反模式2——脏读窗口未加@GlobalLock

错误示例

业务代码在全局事务Phase1提交后,Phase2完成前,另一个非全局事务直接读取了已提交的库存扣减结果,并据此创建订单。读取代码未添加@GlobalLock注解。

// 错误示例:未加@GlobalLock,直接查询库存
@Service
public class OrderService {
    @Autowired
    private JdbcTemplate jdbcTemplate;
    
    // 此方法在非全局事务中运行
    public void createOrder(String productId, int quantity) {
        // 直接读取库存,此时可能读到未全局提交的"脏"数据
        int stock = jdbcTemplate.queryForObject(
            "SELECT stock FROM product WHERE id=?", Integer.class, productId);
        if (stock >= quantity) {
            // 创建订单...
            createOrderRecord(productId, quantity);
        }
    }
}

现象描述

全局事务因后续分支失败而回滚,库存恢复。但订单却基于脏读的库存数据被创建,导致超卖。检查undo_log表可发现该xid的回滚记录(log_status=3),而订单表存在对应记录,且创建时间介于全局事务Phase1和Phase2之间。

排查思路

  1. 确认全局事务回滚
    SELECT * FROM undo_log WHERE branch_id = ? AND log_status = 3; 
    -- 3代表已回滚,branch_id可从Seata日志获取
    
  2. 检查订单创建时间
    SELECT * FROM orders WHERE product_id = ? ORDER BY create_time DESC;
    
    若订单创建时间落在全局事务Phase1之后、Phase2之前,即为脏读导致。
  3. 检查业务代码:读取库存的操作是否添加了@GlobalLockSELECT FOR UPDATE
  4. 关联监控:自定义指标at_dirty_read_count增加,Grafana L2 AT面板显示异常。

根因分析

Seata AT默认隔离级别为读未提交(详见第2篇模块5)。在Phase1本地事务提交后,数据已持久化,但全局事务尚未决议。此时非全局事务读到的是“可能被回滚”的中间状态。@GlobalLock注解通过检查全局锁表(lock_table)来实现读已提交:查询前检测相关行是否被全局锁锁定,若有则等待直到锁释放,从而读到已全局提交的数据。该注解必须配合SELECT FOR UPDATE使用。

修正方案

在读取侧添加@GlobalLock并执行SELECT FOR UPDATE

// 修正示例:加@GlobalLock防止脏读
@GlobalLock
@Transactional(propagation = Propagation.REQUIRES_NEW) // 新事务,独立于全局事务
public int getStockForUpdate(String productId) {
    return jdbcTemplate.queryForObject(
        "SELECT stock FROM product WHERE id=? FOR UPDATE", Integer.class, productId);
}

该方法会阻塞直到全局锁释放,确保读到的是最终提交或回滚后的数据。

最佳实践

  • 所有涉及全局事务数据的读取操作,若不允许脏读,必须加@GlobalLock
  • 对高并发读取场景,可考虑读写分离:写库使用Seata AT,读库通过备库或缓存,避免加锁影响性能。
  • 通过监控指标at_dirty_read_count(自定义)统计脏读风险,P2告警。

4.2 反模式3——undo_log表膨胀

错误示例

Seata AT在生产环境运行半年,undo_log表未配置定期清理,积累数千万行,占用磁盘空间超过100GB。

# 错误配置:未开启定期清理,或使用默认值0(不清理)
seata.log.delete.period=0

现象描述

数据库磁盘使用率>90%,undo_log查询性能下降,影响Seata分支事务的注册和Phase2提交。SHOW TABLE STATUS LIKE 'undo_log'显示Data_length数十GB,Rows达千万级。业务感知为全局事务提交变慢,甚至超时。

排查思路

-- 查看总行数和总数据大小
SELECT COUNT(*) FROM undo_log;
SHOW TABLE STATUS LIKE 'undo_log';
-- 检查7天前的旧日志数量
SELECT COUNT(*) FROM undo_log WHERE log_created < DATE_SUB(NOW(), INTERVAL 7 DAY);

发现大量7天前的日志未被清理。若磁盘使用率超过阈值(如80%),触发P1告警(监控指标undo_log_table_size)。

根因分析

Seata AT在Phase2完成后,会通过UndoLogManager.deleteUndoLog()异步删除undo_log记录(第2篇模块4)。但在高TPS场景下,产生速度大于删除速度,或异步删除线程阻塞、异常导致积压。另外,若未开启定时清理,已提交或回滚的事务日志会永远保留。默认配置下,Seata Client仅异步删除当日日志,不主动清理历史数据。

修正方案

  1. 开启Seata Client的定期清理(推荐):
    # application.properties
    seata.log.delete.period=86400000   # 每24小时执行一次
    seata.log.delete.interval=7        # 保留最近7天的日志
    
  2. 手动执行清理(应急):
    -- 分批删除,每批10000行,避免长事务锁表
    DELETE FROM undo_log WHERE log_created < DATE_SUB(NOW(), INTERVAL 7 DAY) LIMIT 10000;
    -- 重复执行直到不再影响
    
  3. 改为同步删除(权衡):设置seata.undo.log.delete.period=0改为Phase2同步删除,但会增加提交延迟,需评估业务影响。
  4. 升级Seata Server:若使用Seata Server存储undo_log,可配置seata.server.undo.log.save.days自动清理。

最佳实践

  • 监控undo_log表大小和行数(undo_log_table_size指标),P1告警:磁盘使用>80%或行数>500万。
  • 每日定时清理7天前日志,并设置归档策略(可导出到冷存储)。
  • 评估是否可开启同步删除,若TPS在2000以下,同步删除延迟增加不明显。
  • 在手动恢复平台中增加“清理undo_log”功能,以便紧急释放磁盘空间。

九大反模式全景图

flowchart LR
    subgraph "XA"
        A1["In-Doubt阻塞"] -->|"根因: 协调者宕机"| M1["xa_indoubt_transactions_total"]
    end
    subgraph "AT"
        A2["脏读窗口"] -->|"根因: 未加@GlobalLock"| M2["at_dirty_read_count"]
        A3["undo_log膨胀"] -->|"根因: 未清理"| M3["undo_log_table_size"]
    end
    subgraph "TCC"
        A4["悬挂"] -->|"根因: 防悬挂缺失"| M4["tcc_hanging_transactions_total"]
        A5["空回滚"] -->|"根因: 未处理空回滚"| M5["tcc_empty_rollback_errors_total"]
        A6["幂等缺失"] -->|"根因: Confirm/Cancel无幂等"| M6["tcc_duplicate_confirm_total"]
    end
    subgraph "Saga"
        A7["补偿失败"] -->|"根因: 无人工介入"| M7["saga_compensation_failed_total"]
    end
    subgraph "消息"
        A8["SENDING残留"] -->|"根因: 超时无回退"| M8["outbox_sending_stuck_total"]
    end
    subgraph "CDC"
        A9["Binlog错误"] -->|"根因: 格式非ROW"| M9["debezium_connector_status"]
    end
    M1 --> G["Grafana L1/L2面板"]
    M2 --> G
    M3 --> G
    M4 --> G
    M5 --> G
    M6 --> G
    M7 --> G
    M8 --> G
    M9 --> G
    G --> Manual["手动恢复平台"]

    classDef xa fill:#f1f5f9,stroke:#334155,stroke-width:2px,color:#0f172a
    classDef at fill:#e2e8f0,stroke:#475569,stroke-width:2px,color:#1e293b
    classDef tcc fill:#ede9fe,stroke:#8b5cf6,stroke-width:2px,color:#3b2f4b
    classDef saga fill:#fef3c7,stroke:#d97706,stroke-width:2px,color:#78350f
    classDef msg fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#1e3a8a
    classDef cdc fill:#f8d7da,stroke:#721c24,stroke-width:2px,color:#721c24
    classDef ops fill:#f1f5f9,stroke:#64748b,stroke-width:2px,color:#1e293b

    class A1,M1 xa
    class A2,A3,M2,M3 at
    class A4,A5,A6,M4,M5,M6 tcc
    class A7,M7 saga
    class A8,M8 msg
    class A9,M9 cdc
    class G,Manual ops

图4说明

  • 总览:该全景图按方案分组,概括九大反模式、根因、关联的Prometheus监控指标,并汇聚到Grafana面板作为诊断入口。
  • 关联篇号:每个反模式标注了对应前文(第1-7篇)和具体监控指标,方便读者回溯。
  • 排查闭环:告警触发→定位反模式→查看对应L2面板→进入L3单事务追踪→手动恢复平台操作。
  • 监控先行:通过第7篇的告警体系,绝大部分反模式在用户影响扩大前即可被发现。

5. TCC反模式:悬挂未防 + 空回滚未处理 + 幂等缺失(关联第3篇)

5.1 反模式4——悬挂未防

错误示例

Try方法因网络延迟迟迟未到达,Cancel先到达并释放资源。迟到的Try到达后未检查悬挂,直接执行资源预留。

// 错误示例:Try未检查防悬挂表
public boolean tryFreeze(String businessId, BigDecimal amount) {
    // 直接冻结资源,未查询防悬挂表
    accountMapper.freeze(businessId, amount);
    return true;
}

现象描述

用户账户被冻结两次,资金不可用。tcc_fence_log表中同一businessId出现Cancel记录时间早于Try记录。业务表现为用户可用余额减少两倍冻结金额。

排查思路

  1. 查询防悬挂表
    SELECT * FROM tcc_fence_log WHERE business_id = 'biz123' ORDER BY gmt_create;
    -- 发现cancel记录创建时间早于try记录
    
  2. 查看Grafana面板:L2 TCC面板中tcc_hanging_transactions_total指标突增,P1告警。
  3. 追踪xid:进入L3单事务追踪,输入xid,查看Try和Cancel的调用时间线,发现Try调用迟于Cancel。

根因分析

TCC中,TM调用Try超时后会发起Cancel。若Try实际延迟到达,而Cancel已执行并释放资源,迟到的Try就是悬挂操作(详见第3篇模块3)。缺少防悬挂表检查,Try会重复预留资源,导致资源被冻结两次。

修正方案

Try执行前查询防悬挂表,若存在status='CANCELLED'且未过期,则拒绝并返回成功:

// 修正示例:Try前检查悬挂
public boolean tryFreeze(String businessId, BigDecimal amount) {
    TccFenceLog fence = fenceLogMapper.selectByBusinessId(businessId);
    if (fence != null && "CANCELLED".equals(fence.getStatus())) {
        // 悬挂,已Cancel,Try不应执行,直接返回成功以完成事务
        return true;
    }
    // 正常冻结,并插入TRYING记录
    accountMapper.freeze(businessId, amount);
    fenceLogMapper.insert(new TccFenceLog(businessId, "TRYING"));
    return true;
}

Seata TCC可通过useTCCFence=true开启自动防悬挂(由TCCFenceHandler拦截器实现,第3篇模块3)。

最佳实践

  • 使用Seata TCC的内置防悬挂机制TCCFenceHandler,只需配置即可。
  • 防悬挂记录设置过期时间(如30分钟),避免无限增长。
  • 监控tcc_hanging_transactions_total,P1告警。

5.2 反模式5——空回滚未处理

错误示例

Try阶段还未执行或执行失败(如网络丢包),Cancel到达后因找不到Try记录直接抛异常,TM不断重试Cancel。

// 错误示例:Cancel未处理空回滚
public boolean cancelFreeze(String businessId) {
    TccFenceLog tryLog = fenceLogMapper.selectByBusinessIdAndStatus(businessId, "TRYING");
    if (tryLog == null) {
        throw new RuntimeException("Try record not found"); // 导致重试风暴
    }
    // 释放资源...
    return true;
}

现象描述

Cancel接口持续报错,tcc_transaction表无Try记录,TM重试数十次,产生大量告警。第7篇tcc_empty_rollback_errors_total指标升高。

排查思路

  1. 确认Try未执行:查询业务表,无冻结记录。
  2. 检查防悬挂表SELECT * FROM tcc_fence_log WHERE business_id = 'biz456'; 无TRYING记录。
  3. 查看TM重试日志:大量Cancel调用异常栈。
  4. Grafana L2 TCC面板tcc_empty_rollback_errors_total计数器增长。

根因分析

Cancel调用时Try未执行,此时应判定为空回滚,Cancel直接返回成功并写入防悬挂记录(标记CANCELLED),防止后续迟到Try悬挂。若未处理,框架会认为Cancel失败,持续重试,浪费资源并制造告警噪音。

修正方案

public boolean cancelFreeze(String businessId) {
    TccFenceLog tryLog = fenceLogMapper.selectByBusinessId(businessId);
    if (tryLog == null || tryLog.getStatus() == null) {
        // 空回滚:插入CANCELLED记录防悬挂,并返回成功
        fenceLogMapper.insertIfAbsent(new TccFenceLog(businessId, "CANCELLED"));
        return true;
    }
    // 正常回滚...
    accountMapper.unfreeze(businessId);
    fenceLogMapper.updateStatus(businessId, "CANCELLED");
    return true;
}

最佳实践

  • Cancel方法必须实现空回滚判定,Seata TCCFenceHandler同样可自动处理。
  • 设计防悬挂表时,business_id作为唯一索引,用INSERT IGNOREON DUPLICATE KEY UPDATE避免并发冲突。

5.3 反模式6——幂等缺失导致重复扣减

错误示例

Confirm因网络超时被TM重试,第二次Confirm再次扣减冻结库存,无幂等控制。

// 错误示例:Confirm直接操作,无状态检查
public boolean confirmDeduct(String businessId) {
    accountMapper.confirmDeduct(businessId); // 再次扣减,导致金额减少两次
    return true;
}

现象描述

库存变为负数,或用户账户扣款两次。tcc_transaction表中同businessId出现多条CONFIRM记录。

排查思路

SELECT business_id, action_type, COUNT(*) AS cnt 
FROM tcc_fence_log 
WHERE action_type = 'CONFIRM' 
GROUP BY business_id 
HAVING cnt > 1;

若结果非空,说明存在幂等问题。同时检查业务表(库存)出现负数。

根因分析

分布式环境中,网络超时导致TM重试Confirm/Cancel,若业务接口未实现幂等,重复执行会破坏数据正确性。应在防悬挂表或业务表中使用状态机保证幂等。

修正方案

通过乐观锁更新防悬挂表状态:

UPDATE tcc_fence_log SET status = 'CONFIRMED'
WHERE business_id = ? AND status = 'TRYING';

若更新影响行数>0,表示首次执行,继续业务操作;否则表明已执行过(幂等),直接返回成功。

// 修正示例
public boolean confirmDeduct(String businessId) {
    int rows = fenceLogMapper.updateStatus(businessId, "TRYING", "CONFIRMED");
    if (rows > 0) {
        // 首次执行,完成真实扣减
        accountMapper.confirmDeduct(businessId);
    }
    // 幂等返回成功
    return true;
}

最佳实践

  • 所有Confirm/Cancel必须基于唯一键+状态机保证幂等。
  • 监控tcc_duplicate_confirm_total指标,P2告警。
  • 防悬挂表business_id唯一,状态流转:TRYING -> CONFIRMED/CANCELLED

TCC悬挂与空回滚故障推演图

sequenceDiagram
    participant TM as 事务管理器
    participant Try as Try服务
    participant Cancel as Cancel服务
    participant DB as 防悬挂表
    TM->>Try: Try(biz123)
    Note over Try: 网络延迟,Try未收到?
    TM->>Cancel: 超时,调用Cancel
    Cancel->>DB: 查询biz123
    DB-->>Cancel: 无TRYING记录
    Note over Cancel: 判为空回滚
    Cancel->>DB: INSERT biz123, CANCELLED
    Cancel-->>TM: 成功
    Try->>DB: 迟到,查询biz123
    DB-->>Try: 存在CANCELLED
    Note over Try: 判为悬挂,拒绝执行
    Try-->>TM: 成功(悬挂防御)

图5说明

  • 总览:该时序图同时展示了TCC空回滚和悬挂的防御逻辑。
  • 空回滚:Cancel到达时未找到Try记录,直接写入CANCELLED状态并返回成功。
  • 悬挂防御:迟到的Try检测到CANCELLED记录,拒绝执行业务逻辑但返回成功,保证TM事务完成。
  • 关联源码:详细防悬挂拦截器见第3篇模块3TccFenceHandler
  • 监控:若未实现该逻辑,会导致tcc_hanging_transactions_total上升,触发P1告警。

6. Saga反模式:补偿失败无人工介入(关联第4篇)

错误示例

Saga编排的订单取消流程:T1扣减库存成功,T2取消订单,但补偿C2(释放库存)因下游库存服务长期不可用,重试耗尽后,事务停留在中间状态,无人工处理机制。

// Saga编排未定义失败回调
@SagaOrchestration
public void cancelOrder(Order order) {
    step1.inventoryDeduct();    // T1 成功
    step2.cancelOrder();        // T2 成功
    // 若释放库存补偿C1失败,无后续处理
}

现象描述

订单状态为“已取消”,但库存未释放,用户无法重新下单。saga_transaction_log表状态为COMPENSATION_FAILEDsaga_compensation_failed_total指标升高,P1告警。

排查思路

  1. 查询Saga事务日志
    SELECT * FROM saga_transaction_log WHERE status = 'COMPENSATION_FAILED' AND gmt_modified < NOW() - INTERVAL 10 MINUTE;
    
  2. 检查业务数据:订单状态为“CANCELLED”,库存扣减记录未回滚。
  3. Grafana L2 Saga面板:查看补偿失败数量和趋势。

根因分析

Saga的补偿事务Ci可能因下游服务长期不可用或业务逻辑错误而失败,重试策略耗尽后,框架无法自动恢复,需要人工干预(向前恢复或向后补偿)。若没有人工介入流程,数据将长期不一致(第4篇模块5)。例如,库存服务down机超过重试次数,补偿停摆。

修正方案

  1. 编排框架(如Camunda)在补偿失败后触发告警,并将事务标记为FAILED
  2. 手动恢复平台提供操作:通过Admin后台查询失败事务,提供“重试补偿”、“向前恢复”、“跳过补偿”按钮(第7篇)。
  3. 设计“向前恢复”接口:当补偿不可行时,通过业务手段修复,如人工退款、发送客服工单。
// 补偿失败监听器,发送告警并记录
@Component
public class SagaCompensationListener {
    @SagaCompensationFailed
    public void onCompensationFailed(SagaTransaction tx, Exception e) {
        alertService.send("Saga补偿失败", tx.getXid(), e.getMessage());
        sagaLogMapper.updateStatus(tx.getXid(), "FAILED");
    }
}

最佳实践

  • 必须配备补偿失败P1告警(第7篇告警规则)。
  • 手动恢复平台提供“重试补偿”、“向前恢复”、“跳过补偿”操作(第7篇手动恢复平台)。
  • Saga定义时明确“向前恢复”策略作为最终兜底(如人工流程、运营工具)。
  • 记录详细补偿日志,包括每次重试的输入、输出、异常堆栈,便于人工分析。

7. 消息反模式:outbox表SENDING状态残留(关联第5篇)

错误示例

本地消息表发件箱模式中,OutboxScheduler在发送消息时宕机,消息状态停留在SENDING。重启后,调度器只扫描PENDING状态,该消息永久滞留,消费者永远收不到消息。

-- 发件箱表结构
CREATE TABLE outbox (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    aggregate_id VARCHAR(64),
    event_type VARCHAR(128),
    payload TEXT,
    status VARCHAR(20) NOT NULL DEFAULT 'PENDING', -- PENDING, SENDING, SENT
    next_retry_time DATETIME NOT NULL,
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP
);

调度器SQL:

-- 错误:只扫描PENDING,忽略SENDING超时
SELECT * FROM outbox WHERE status = 'PENDING' AND next_retry_time <= NOW() LIMIT 100;

现象描述

订单已创建,但下游服务从未收到消息。outbox表中存在大量status='SENDING'next_retry_time已过去的记录。第7篇outbox_sending_stuck_total指标飙升,P2告警。

排查思路

-- 发现被卡住的消息
SELECT COUNT(*) FROM outbox WHERE status = 'SENDING' AND next_retry_time < NOW() - INTERVAL 5 MINUTE;

若结果>0,说明有残留。检查发送线程日志,确认宕机前正在处理这批消息。监控指标outbox_sending_stuck_total值对应残留数。

根因分析

SENDING状态表示消息正在发送中,但发送线程异常退出后,状态无法自动回退。调度器只扫描PENDING,导致这些消息成为“僵尸”记录(第5篇模块1)。这违反了状态机的完整性,缺少超时回退机制。

修正方案

增加定时任务(可集成在调度器中)扫描超时的SENDING记录并重置为PENDING

-- 超时回退:将超过5分钟仍为SENDING的记录重置为PENDING,立即重试
UPDATE outbox SET status = 'PENDING', next_retry_time = NOW()
WHERE status = 'SENDING' AND next_retry_time < NOW() - INTERVAL 5 MINUTE;

同时,调度器应扫描PENDINGSENDING超时记录(在应用层判断状态和时间)。

最佳实践

  • SENDING状态必须有超时回退,回退间隔取决于消息发送平均耗时(如5分钟)。
  • 监控SENDING状态消息数量,设置阈值告警(第7篇P2)。
  • 发送逻辑优化:发送前UPDATE status = 'SENDING', next_retry_time = NOW()+5min,发送成功后DELETE或标记SENT;若发送失败或超时,回退为PENDING并设置重试时间。
  • 采用至少一次投递语义,消费端需幂等。

8. CDC反模式:Binlog格式错误(关联第6篇)

错误示例

MySQL Binlog格式配置为STATEMENT,Debezium Connector启动时解析Binlog失败,状态变为FAILED

-- 错误的binlog_format
SET GLOBAL binlog_format = 'STATEMENT';

现象描述

Debezium Connector日志报错:binlog_format must be ROWGET /connectors/outbox-connector/status返回{"state":"FAILED","trace":"...binlog_format..."}。CDC数据管道中断,所有通过CDC发件箱投递的消息丢失。

排查思路

  1. 检查MySQL配置
    SHOW VARIABLES LIKE 'binlog_format';
    
    输出为STATEMENTMIXED
  2. 查看Debezium Connector状态
    curl -s http://kafka-connect:8083/connectors/outbox-connector/status | jq .
    
  3. 检查第7篇监控debezium_connector_status指标为0(非RUNNING),触发P0告警。

根因分析

Debezium依赖ROW格式的Binlog来获取行级变更的beforeImageafterImage(第6篇模块1)。STATEMENT格式只记录SQL语句,无法还原具体行变化,尤其在使用NOW()等非确定性函数时。MIXED格式也不保证所有变更都记录为ROW。因此Debezium强制要求binlog_format=ROW

修正方案

SET GLOBAL binlog_format = 'ROW';
SET GLOBAL binlog_row_image = 'FULL';  -- 确保完整前镜像

修改后需重启MySQL使全局设置对新连接生效(5.7/8.0版本动态修改即时生效,但建议重启确认)。然后重启Debezium Connector:

curl -X POST http://kafka-connect:8083/connectors/outbox-connector/restart

最佳实践

  • MySQL部署规范中强制binlog_format=ROWbinlog_row_image=FULL
  • 启动Debezium前,通过脚本自动检查Binlog格式,不满足则拒绝启动并告警。
  • 监控Debezium Connector状态(debezium_connector_status指标),P0告警:Connector非RUNNING。
  • CDC发件箱模式要求MySQL 8.0开启GTID,并配置enforce_gtid_consistency=ON

9. 反模式与监控指标的联动关系(关联第7篇)

每个反模式都对应一组监控指标和标准排查路径,形成“告警→面板→追踪→恢复”闭环。

反模式与监控指标联动关系图

flowchart LR
    subgraph "反模式"
        AP1["XA: In-Doubt阻塞"]
        AP2["AT: 脏读窗口"]
        AP3["AT: undo_log膨胀"]
        AP4["TCC: 悬挂"]
        AP5["TCC: 空回滚"]
        AP6["TCC: 幂等缺失"]
        AP7["Saga: 补偿失败"]
        AP8["消息: SENDING残留"]
        AP9["CDC: Binlog错误"]
    end
    subgraph "监控指标(Prometheus)"
        M1["xa_indoubt_transactions_total"]
        M2["at_dirty_read_count"]
        M3["undo_log_table_size"]
        M4["tcc_hanging_transactions_total"]
        M5["tcc_empty_rollback_errors_total"]
        M6["tcc_duplicate_confirm_total"]
        M7["saga_compensation_failed_total"]
        M8["outbox_sending_stuck_total"]
        M9["debezium_connector_status"]
    end
    subgraph "告警级别"
        L1["P0 - 紧急"]
        L2["P1 - 严重"]
        L3["P2 - 警告"]
    end
    subgraph "Grafana面板"
        G1["L1全局概览"]
        G2["L2方案细分-XA"]
        G3["L2-AT"]
        G4["L2-TCC"]
        G5["L2-Saga"]
        G6["L2-消息"]
        G7["L2-CDC"]
        G8["L3单事务追踪"]
    end
    AP1 --> M1 --> L1 --> G1 --> G2
    AP2 --> M2 --> L3 --> G3 --> G8
    AP3 --> M3 --> L2 --> G3
    AP4 --> M4 --> L2 --> G4
    AP5 --> M5 --> L3 --> G4
    AP6 --> M6 --> L3 --> G4
    AP7 --> M7 --> L2 --> G5
    AP8 --> M8 --> L3 --> G6
    AP9 --> M9 --> L1 --> G7
    G2 --> Manual["手动恢复平台"]
    G3 --> Manual
    G4 --> Manual
    G5 --> Manual
    G6 --> Manual
    G7 --> Manual

    classDef ap fill:#f1f5f9,stroke:#334155,stroke-width:2px,color:#0f172a
    classDef metrics fill:#e2e8f0,stroke:#475569,stroke-width:2px,color:#1e293b
    classDef alert fill:#ede9fe,stroke:#8b5cf6,stroke-width:2px,color:#3b2f4b
    classDef panel fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#1e3a8a
    classDef manual fill:#fef3c7,stroke:#d97706,stroke-width:2px,color:#78350f

    class AP1,AP2,AP3,AP4,AP5,AP6,AP7,AP8,AP9 ap
    class M1,M2,M3,M4,M5,M6,M7,M8,M9 metrics
    class L1,L2,L3 alert
    class G1,G2,G3,G4,G5,G6,G7,G8 panel
    class Manual manual

图6说明

  • 总览:反模式(左)映射到特定监控指标(中),关联告警级别(P0/P1/P2)和Grafana面板(L1/L2/L3),最后指向手动恢复平台。
  • 排查标准路径:例如,tcc_hanging_transactions_total突增触发P1告警→运维人员在Grafana L1概览发现TCC异常→深入L2 TCC面板,发现悬挂事务数量暴增→进入L3单事务追踪,筛选悬挂xid,查看防悬挂表时间线→通过Admin后台执行“释放悬挂资源”或“跳过”。
  • 告警分级:In-Doubt和CDC状态异常直接影响全局,设为P0;悬挂、undo_log膨胀、补偿失败为P1;脏读窗口、SENDING残留等为P2。
  • 联动价值:这套联动机制将反模式从“被动救火”升级为“主动发现+快速诊断+一键恢复”(第7篇手动恢复平台操作)。

10. 面试高频专题

Q1: 六种分布式事务方案(XA/AT/TCC/Saga/消息/CDC)各适合什么场景?如何快速选型?

核心回答:通过七步决策树快速收敛:秒级强一致且TPS<500用XA;500-3000用AT;3000-5000且可接受业务侵入用TCC;长事务用Saga;异步通知用消息或CDC。关键是平衡一致性、性能、侵入性。

详细解释:选型需首先分析业务特征:事务跨度、一致性要求、TPS量级、团队对侵入性的接受度、是否有资源预留概念、现有基础设施。七步决策树将这些考量固化为流程。例如,电商订单状态流转属于异步最终一致高吞吐,直接走向消息方案;资金转账是秒级强一致中高并发,需要TCC;跨国汇款是长事务,只能Saga。矩阵雷达图直观展示了各方案在一致性、性能、延迟、锁持有等维度的权衡,XA强一致但吞吐极低,Saga/消息吞吐高但一致性弱。最终选型时,还需考虑团队运维能力,本地消息表最简单,CDC最复杂。

多角度追问

  1. “TPS上限是绝对值吗?受哪些因素影响?”—— 不是绝对值,受数据库性能、网络延迟、业务SQL复杂度、锁粒度影响。压测条件为4C8G MySQL千兆网络单表CRUD,实际生产可能折半或更低。
  2. “如果不希望侵入业务,是否只能选AT?其脏读窗口如何解决?”—— AT零侵入但存在脏读,必须对关键读取加@GlobalLock,这本身是轻度侵入。若完全不能接受任何侵入,可考虑XA(牺牲性能)或消息(异步场景)。
  3. “已有Kafka但无Debezium运维经验,是否直接用本地消息表?”—— 建议先用本地消息表,稳定后演进到CDC,因为CDC运维复杂度高,需要专人管理Kafka Connect和Binlog。

加分回答:结合业务发展周期选型。创业初期用本地消息表快速上线;成长期引入Seata AT/TCC解决强一致需求,并搭建监控体系;成熟期可升级到CDC发件箱以降低延迟。决策树中Step7基础设施复用非常关键,切忌重复造轮子。

Q2: XA的In-Doubt Transaction是如何产生的?如何监控和手动恢复?

核心回答:协调者在2PC的prepare后、commit前宕机,资源管理器上的事务分支处于Prepared状态并持有锁,成为In-Doubt事务。监控xa_indoubt_transactions_total>0触发P0告警。通过xa recover列出,根据业务确认手动xa commitxa rollback,或在Admin后台强制处理。

详细解释:2PC协议中,协调者先向所有参与者发送prepare,参与者执行事务但不提交,锁住资源并持久化undo/redo,然后回复yes。若协调者在发出commit决策前宕机,参与者将一直等待,锁无法释放。MySQL可通过xa recover命令查看这些Prepared的xid。恢复方式:若有协调者事务日志,可重启协调者自动完成;否则必须人工介入,通过查询业务日志或上下游判断该提交还是回滚,再执行xa commit 'xid'xa rollback 'xid'。Atomikos等JTA实现可将事务日志存库,实现协调者HA。

多角度追问

  1. “如果无法确认该提交还是回滚,怎么办?”—— 优先考虑回滚,因为回滚可释放锁且不破坏数据完整性(相当于未发生),但需业务容忍短暂不一致。可通过业务补偿机制修正。
  2. “协调者HA如何实现?Atomikos如何恢复事务日志?”—— Atomikos支持使用数据库存储事务日志,多实例共享该日志,宕机后另一实例接管恢复。
  3. “与Seata AT的全局锁相比,XA锁持有时间更长,为什么?”—— XA的锁从prepare持续到commit/rollback,而AT在Phase1本地事务提交后就释放了本地锁,全局锁虽持续到Phase2,但仅用于防止脏写,粒度不同。

加分回答:生产环境In-Doubt监控不仅要看数量,还要看持续时间。可使用SELECT * FROM information_schema.innodb_trx WHERE trx_state='PREPARED' AND trx_started < NOW() - INTERVAL 5 MINUTE,超过5分钟即告警。

Q3: Seata AT的脏读窗口是什么?如何通过@GlobalLock防御?

核心回答:AT模式下,全局事务Phase1本地提交后至Phase2全局提交前,其他事务可读到未全局提交的数据,即为脏读窗口。通过在被读数据的查询方法上添加@GlobalLock + SELECT FOR UPDATE,使读取操作等待全局锁释放,实现读已提交。

详细解释:Seata AT默认隔离级别为读未提交(详见第2篇模块5)。在Phase1,分支事务提交并注册全局锁,此时本地数据已可见,但全局事务可能最终回滚。若另一个非全局事务在Phase2之前读取了这些数据并做出写决策,就会产生脏读。@GlobalLock会先检查lock_table中是否存在该记录的全局锁,若有则阻塞等待,直到锁被释放(全局事务决议)。这就保证了读取的数据是已经全局提交或回滚后的,相当于读已提交隔离级别。注意该注解必须结合SELECT FOR UPDATE

多角度追问

  1. @GlobalLock@GlobalTransactional的关系?”—— @GlobalLock用于非全局事务的读取隔离,@GlobalTransactional用于发起全局事务。两者独立,但可配合。
  2. “是否所有读操作都必须加?会不会造成性能问题?”—— 并非所有读都需加,只有那些要求强一致读(不允许读到中间态)的场景才加。加锁会阻塞并发读,性能下降,可通过读写分离或缓存缓解。
  3. “如果未加,发生了超卖,如何通过undo_log回滚?”—— 若全局事务回滚,AT会自动根据undo_log生成逆向SQL恢复数据,但已基于脏读创建的订单不会自动修复,需要人工处理(如退款)。

加分回答:AT的脏读问题本质是分布式快照隔离的缺失,@GlobalLock是一种“悲观读锁”解决方案。若希望乐观,可采用版本号机制,但会增加复杂度。

Q4: Seata AT的undo_log表为什么会膨胀?如何清理和监控?

核心回答:高TPS下undo_log产生速度超过异步删除速度,或未开启定期清理,导致历史日志堆积。需开启seata.log.delete.period定时清理(如保留7天),监控undo_log_table_size,P1告警。

详细解释:AT分支事务结束时,会异步删除undo_log(通过UndoLogManager.deleteUndoLog),但异步线程可能因阻塞、异常或消费速度跟不上而积压。另外,若未配置定时清理任务,已提交和已回滚的日志将永久保留。长期运行的AT系统,undo_log表可达数百GB。清理措施包括:在application.properties中配置seata.log.delete.period=86400000seata.log.delete.interval=7,让Client每日清理7天前的日志;紧急时可手动分批DELETE。监控方面,可使用Prometheus采集information_schema.tablesdata_lengthtable_rows,超过阈值告警。

多角度追问

  1. “异步删除失败的兜底策略?”—— 可配合定时任务清理,同时监控异步删除失败率,当失败率升高时告警并人工介入。
  2. “修改为同步删除对性能的影响?”—— 同步删除会让每个分支事务增加一次DELETE,延迟增加约2-5ms,对TPS 1000以下影响不大,高并发下可能成为瓶颈。
  3. “如何通过Grafana面板监控undo_log大小?”—— 面板上配置SQL数据源,或通过JMX暴露seata.undo.log.count指标,在L2 AT面板展示趋势图。

加分回答:还可使用MySQL分区表按时间范围分区,自动归档旧数据,结合定期TRUNCATE PARTITION,比DELETE高效得多,适合超大容量场景。

Q5: TCC的悬挂、空回滚、幂等缺失三种异常各是什么?如何正确防御?

核心回答

  • 悬挂:Cancel先于Try到达,迟到的Try重复预留资源。防:Try前查防悬挂表,若已有CANCELLED记录则拒绝。
  • 空回滚:Cancel调用时Try未执行,直接抛异常导致重试风暴。防:Cancel判空,写CANCELLED返回成功。
  • 幂等缺失:Confirm/Cancel重试导致重复操作。防:通过防悬挂表状态机TRYING→CONFIRMED/CANCELLED保证一次执行。

详细解释:这三种异常是TCC模型固有的并发问题(第3篇模块3)。根源在于网络延迟、超时和重试。防御手段统一依赖“防悬挂表”(tcc_fence_log),以业务主键businessId为唯一索引。悬挂:Try到来时查询是否存在CANCELLED记录,存在则说明已Cancel,直接返回成功。空回滚:Cancel到来时无TRYING记录,插入CANCELLED并返回成功,避免TM认为失败而重试。幂等:Confirm/Cancel通过乐观锁更新状态(UPDATE ... SET status='CONFIRMED' WHERE status='TRYING'),影响行数>0才执行业务操作。Seata TCC的TCCFenceHandler已内置这些逻辑。

多角度追问

  1. “Seata TCC的TCCFenceHandler如何自动实现这些?”—— 通过AOP拦截Try/Confirm/Cancel方法,在业务逻辑前后操作防悬挂表,实现上述状态检查和幂等。
  2. “防悬挂表的过期时间如何设置?”—— 建议设置为TM的超时重试最大时间窗口,例如5分钟,过期的记录可被清理或忽略,防止新的事务被旧记录阻塞。
  3. “如果防悬挂表挂了,有什么降级方案?”—— 降级为业务表自身的状态机,但会侵入业务;也可暂时关闭防悬挂检查(风险高),待防悬挂表恢复后补偿。

加分回答:TCC的异常处理性能直接关系到方案可用性,面试时可画悬挂时序图加深理解,并强调监控tcc_hanging_transactions_total等指标的重要性。

Q6: Saga的补偿失败如何处理?为什么需要人工介入?

核心回答:Saga的补偿Ci重试耗尽后失败,事务卡在中间态,数据不一致。需通过P1告警通知,运维人员在手动恢复平台执行“重试补偿”或“向前恢复”(如人工退款)。人工介入是兜底策略,因为完全自动恢复不可行。

详细解释:Saga通过补偿事务实现回滚,但补偿本身可能因下游服务长期不可用或数据冲突而失败。尽管框架可以重试,但不能无限重试,必须设定上限。重试耗尽后,事务进入FAILED状态,需要人工判断如何修复:可以重新触发补偿(下游已恢复),或者执行向前恢复(从当前状态正向完成事务,如取消订单后直接退款而不是补偿库存)。人工介入的必要性在于,机器无法理解业务语义来决定是继续补偿还是正向修复,也无法处理需要人工客服介入的场景。

多角度追问

  1. “如何设计向前恢复接口?”—— 提供一个REST端点,接收xid,执行预定义的向前修复逻辑(如调用退款服务、发送通知),需要做幂等。
  2. “补偿失败的事务如何影响业务?举例说明。”—— 如订单已取消,库存未释放,商品可售卖数减少,导致少卖。需紧急人工处理。
  3. “是否有可能实现全自动恢复?”—— 对于确定性的补偿失败(如资源暂时不可用),可全自动重试;对于业务逻辑错误(如库存已变更),只能人工。可结合规则引擎,部分场景自动处理。

加分回答:完整的Saga系统应包含“补偿失败率”监控、自动升级机制(多次重试失败后自动创建工单),并与第7篇的手动恢复平台打通。

Q7: 本地消息表的SENDING状态残留是什么原因?如何修复和预防?

核心回答:发送线程宕机导致消息状态停留在SENDING,而调度器只扫描PENDING,形成永久残留。需增加超时回退:定时将超过5分钟的SENDING记录重置为PENDING,并监控outbox_sending_stuck_total

详细解释:发件箱模式中,消息先插入为PENDING,调度器取出改为SENDING并发送,成功后删除。若调度器在SENDING状态时宕机,该记录无法自动恢复。因为常规扫描只查PENDING,这些记录变为“僵尸”。修复方法是引入超时回退机制,由一个定时任务或调度器自身定期执行UPDATE outbox SET status='PENDING', next_retry_time=NOW() WHERE status='SENDING' AND next_retry_time < NOW() - INTERVAL 5 MINUTE。这样即使宕机,超时后也能重新投递。

多角度追问

  1. “为什么需要SENDING状态?不能直接删除?”—— SENDING状态用于防止重复投递(at least once),如果发送后删之前宕机,重启后可能重复发送,消费端需幂等。SENDING标识正发送中,避免并发重复发送。
  2. “超时回退和业务死信队列如何配合?”—— 可设最大重试次数,超过后移入死信表,人工处理。
  3. “RocketMQ事务消息是否也存在类似问题?”—— RocketMQ事务消息由MQ Server管理半消息状态,有超时回查机制,不会出现SENDING残留,但需要处理回查逻辑。

加分回答:可在发件箱表增加version字段,用乐观锁更新状态,避免并发更新冲突。

Q8: CDC发件箱的Binlog格式必须是ROW吗?STATEMENT格式会导致什么问题?

核心回答:必须是ROW格式。STATEMENT格式只记录SQL语句,Debezium无法获取变更前后的行数据(before/after image),导致无法解析事件,Connector启动失败或丢失数据。

详细解释:Debezium作为CDC工具,通过解析MySQL Binlog来捕获数据变更。ROW格式记录每一行修改前后的具体值,Debezium据此构建SourceRecord,包含操作类型(INSERT/UPDATE/DELETE)和行数据。STATEMENT格式记录的是执行的SQL文本,不含具体行数据,且对于包含NOW()等非确定性函数的SQL,无法准确还原数据变化,因此Debezium强制要求binlog_format=ROW。若配置错误,Connector会在启动时校验并报错,状态变为FAILED。

多角度追问

  1. “为什么ROW格式对CDC至关重要?”—— CDC需要精确的行级变化用于同步到其他系统,ROW提供确定性的数据,STATEMENT无法满足。
  2. “如何在线修改Binlog格式?”—— SET GLOBAL binlog_format = 'ROW'; 即时生效,但已有连接不会改变,需重启应用连接或刷新。
  3. “使用MIXED格式可以吗?”—— 不推荐,MIXED下MySQL可能在某些情况使用STATEMENT,Debezium仍可能出错,ROW最稳妥。

加分回答:生产环境还须设置binlog_row_image=FULL,确保UPDATE语句的前镜像包含所有列,避免Debezium无法构建完整事件。

Q9: 分布式事务的监控指标体系包括哪些核心指标?P0/P1/P2告警如何分级?

核心回答:核心指标包括:全局事务成功率/耗时/P99、In-Doubt数量、悬挂事务数、死信堆积量、补偿失败次数、全局锁超时次数、CDC延迟等。P0:In-Doubt>0、Connector非RUNNING、全局成功率低于阈值;P1:悬挂>阈值、补偿失败增加、undo_log膨胀;P2:SENDING残留、脏读计数等。

详细解释:监控体系分为三个层次(第7篇)。L1全局概览关注整体健康:global_tx_success_rate(成功率)、global_tx_duration_p99(延迟)、in_doubt_tx_count。P0告警是紧急事件,需要立即处理,例如In-Doubt事务阻塞全库。P1是严重问题,可能导致局部业务受损,如悬挂事务增长、补偿失败堆积。P2是警告,表示存在潜在风险,如发件箱残留增多。告警规则通过Prometheus配置,如xa_indoubt_transactions_total > 0持续1分钟即P0。

多角度追问

  1. “如何通过Prometheus + Grafana实现这些指标?”—— 应用引入Micrometer,将自定义指标注册到MeterRegistry,暴露/actuator/prometheus端点,Prometheus抓取,Grafana配置Dashboard。
  2. “死信堆积如何监控和处理?”—— 死信队列消息数超过阈值即告警,在Admin后台提供死信查询和重投功能。
  3. “手动恢复平台如何与告警联动?”—— 告警信息中包含直达L3面板的链接,点击即可跳转查看异常事务详情,并提供“一键恢复”按钮。

加分回答:还可增加业务维度指标,如各方案占比、各业务线事务成功率,帮助分析方案选型是否合理。

Q10: 如何通过七步决策树为业务选择合适的分布式事务方案?请举一个完整的决策路径案例。

核心回答:以“订单支付后发短信通知”为例:异步→最终一致→高TPS→零侵入→无Kafka→本地消息表。以“用户账户转账”为例:秒级→强一致→TPS 3000→可接受侵入→有冻结概念→TCC→已有Seata Server则复用。

详细解释:决策树将模糊的需求转化为清晰的路径。案例一:发短信是异步通知,不需要强一致,高吞吐,不希望改业务代码,没有Kafka,也没有RocketMQ,最终选择本地消息表,只需一张outbox表和一个调度器。案例二:转账必须同步返回结果,强一致,TPS要求3000,可以侵入代码实现TCC,业务上有冻结资金概念(Try冻结,Confirm扣减,Cancel解冻),公司已部署Seata Server,因此直接使用Seata TCC,复用TC集群。整个推演过程串联了前7篇所有知识。

多角度追问

  1. “如果订单状态需要强一致呢?”—— 那就不适用异步消息,可能需改为AT或TCC,但会牺牲吞吐量,需要产品确认是否接受。
  2. “TPS 5000+但要求强一致如何处理?”—— TCC可勉强支撑,但需优化;或改造业务通过对账实现最终一致,以换取高吞吐。
  3. “决策树是否适用于所有业务?”—— 提供一般指导,特殊业务(如涉及多个异构系统)可能需要混合方案,比如主流程Saga,子流程TCC。

加分回答:决策时还要考虑团队学习曲线,如果团队从未用过TCC,宁愿先用AT+@GlobalLock,再逐步演进。

Q11: 在高并发场景下(TPS 5000+),为什么XA和AT不可行?TCC和Saga各有什么取舍?

核心回答:XA全程锁持有,并发度极低;AT全局锁到Phase2结束,TPS上限约3000。TCC无全局锁,Saga无锁,均可支撑5000+。取舍:TCC业务侵入高,需资源预留;Saga适合长事务,补偿失败需人工介入。

详细解释:锁持有时间是决定并发度的关键因素。XA的锁从prepare持续到commit,期间所有竞争行都被阻塞,TPS很难突破500。AT在Phase1结束后释放本地锁,但全局锁仍然阻止其他全局事务对同行的写操作,竞争激烈时TPS上限在3000左右。TCC在Try阶段仅做资源预留(如冻结),预留完成后即释放业务锁,后续Confirm/Cancel不锁资源,因此可支持5000+。Saga完全没有锁,每步都独立提交,吞吐量最高,但一致性较弱,且补偿链复杂。选择时,若业务允许资源预留且能接受开发侵入,TCC是最佳高性能强一致方案;若业务为长事务且无法预留,只能用Saga。

多角度追问

  1. “如果业务无法做资源预留,TCC是否彻底不能用?”—— 是的,TCC核心就是Try阶段的资源预留,无法预留则不适合。
  2. “Saga在秒级事务中是否有优势?”—— Saga优势在于长事务和异构系统,秒级事务使用Saga会引入额外的编排开销和最终一致性问题,不如直接用AT或TCC。
  3. “如何评估AT的调优上限?”—— 可通过增加Seata Server集群、优化lock_table结构(如分片)、业务侧减少锁冲突来提升,但很难超过4000。

加分回答:实际业务中,可组合使用:核心交易链路用TCC保证强一致高并发,非核心通知用消息异步,长流程用Saga。

Q12: 什么是分布式事务的“锁持有时间”?XA/AT/TCC/Saga的锁持有时间各是多少?为什么逐渐缩短?

核心回答:锁持有时间指事务从获取锁到释放锁的时间窗口,直接影响并发度。XA全程持有(~10-50ms+业务时间);AT本地锁到Phase1结束,全局锁到Phase2(10ms);TCC Try结束释放(<5ms);Saga无锁。逐步缩短源于架构从数据库自动管理向业务显式管理演进。

详细解释:XA使用数据库行锁,在2PC期间全程持有,直至Commit或Rollback,是最重的锁。AT通过分离本地锁和全局锁,Phase1提交后即释放本地锁,全局锁仅用于隔离全局事务间的写冲突,比XA轻。TCC将锁提升到业务层,Try阶段通过业务手段(如冻结库存、状态字段)实现资源预留,完毕后立即释放数据库锁,后续Confirm/Cancel无需锁,因此锁持有时间最短。Saga完全无锁,通过正向提交和反向补偿实现最终一致,但代价是可能出现脏读和不一致窗口。锁持有时间越短,系统并发能力越强,但相应的一致性保障和开发成本也增加。

多角度追问

  1. “锁持有时间受网络延迟影响吗?”—— 受网络影响,XA的prepare和commit需要网络往返,延迟增加则锁持有时间延长。
  2. “如何监控锁等待?与In-Doubt阻塞关联?”—— 可通过SHOW ENGINE INNODB STATUSinformation_schema.innodb_lock_waits监控,In-Doubt是锁持有的极端情况。
  3. “全局锁与本地锁的区别?”—— 本地锁是数据库行锁,全局锁是Seata在lock_table中管理的逻辑锁,用来协调不同服务间的写冲突。

加分回答:面试时可结合第2篇锁管理器源码,讲解LockManager如何尝试获取全局锁,以及超时机制。

Q13: 分布式事务的反模式与监控指标之间如何联动?告警触发后如何通过Grafana面板快速定位?

核心回答:每个反模式对应特定指标(如悬挂→tcc_hanging_transactions_total),触发P1告警。运维进入L1概览确认异常→下钻L2对应方案面板→查看指标趋势和异常列表→进入L3单事务追踪,按xid查看调用链和防悬挂表记录→在Admin后台一键修复。

详细解释:这套联动机制设计在第7篇。例如,TCC悬挂告警触发后,L1面板显示TCC区域异常红标,点击进入L2 TCC面板,可看到悬挂事务数时间序列突增、防悬挂表异常记录Top N。通过L3单事务追踪输入起止时间,过滤action_type=TRYstatus=HANGING的事务,查看其xid的时间线:Cancel先于Try。最后在手动恢复平台查询该xid,执行“释放悬挂资源”操作(删除防悬挂记录并补偿业务)。整个过程不超过3分钟,实现了监控驱动的自动化排查。

多角度追问

  1. “如何在Prometheus中配置告警规则?”—— 使用PromQL定义,如tcc_hanging_transactions_total > 10for: 1m,发送到Alertmanager。
  2. “L1/L2/L3面板的具体查询语句?”—— L1使用sum(rate(...))聚合全局指标,L2按方案筛选标签,L3通过xidbusiness_id作为变量进行下钻。
  3. “手动恢复平台如何实现防悬挂资源释放?”—— 提供一个API,传入xid,后台查询防悬挂表并执行业务补偿(如解冻资金),同时更新防悬挂状态为MANUAL_FIXED

加分回答:可展示Grafana面板JSON模型片段,体现工程实践细节。

Q14: 系统设计题:电商平台三个场景的分布式事务选型与全局架构

场景:(1)下单扣库存:同步,TPS 3000,强一致;(2)订单状态变更通知:异步,TPS 8000,最终一致;(3)跨国汇款:事务跨度5-30分钟,强最终一致。

要求:(A)三个场景方案选型与理由;(B)完整决策树推演过程;(C)监控指标与告警规则设计;(D)可能出现的反模式及预防措施;(E)统一手动恢复平台支持。

核心回答

  • 场景1:推荐TCC。理由:秒级同步、强一致、TPS 3000、有冻结库存概念,可侵入开发。决策树路径:Step1秒级→Step2强一致→Step3 3000→Step4可接受侵入→Step5有资源预留→TCC→Step7若有Seata Server则复用。备选AT+@GlobalLock。
  • 场景2:推荐本地消息表(若无Kafka)或CDC发件箱(有Kafka)。理由:异步最终一致、高吞吐、零侵入。决策树:Step1异步→Step6有Kafka?是→CDC;否→Step7有RocketMQ?否→本地消息表。
  • 场景3:推荐Saga。理由:长事务、无法冻结资金(跨行)、最终一致。决策树:Step1分钟级→Saga→Step7若已有编排引擎(Camunda)则复用。

详细解释(仅展开关键部分):

  • 场景1 TCC设计:Try阶段冻结库存(stock_freeze),Confirm扣减库存并创建订单,Cancel解冻。防悬挂表tcc_fence_log,幂等通过状态机实现。监控指标:全局事务成功率、tcc_hanging_transactions_total(悬挂)、tcc_duplicate_confirm_total(幂等)。告警:P1悬挂>10/分钟,P2重复确认>0。反模式预防:启用Seata TCCFence,定时检查悬挂表。
  • 场景2 消息设计:订单状态变更后写入outbox表,调度器投递到MQ,下游消费。决策树:Step1异步→Step2最终一致→Step3 TPS 8000→Step4零侵入→Step6有Kafka?假设无→Step7有RocketMQ?假设无→本地消息表。监控:outbox_sending_stuck_total、死信堆积。反模式:SENDING残留,设置5分钟超时回退。P2告警。
  • 场景3 Saga设计:汇款流程为T1扣款(本方)、T2入账(中间行)、T3入账(目标行)。失败时逆序补偿:C3退款、C2退款、C1退款。使用Camunda编排,补偿失败进入人工队列。监控:saga_compensation_failed_total,P1告警。反模式:补偿失败无人工介入,必须配备手动恢复平台(重试补偿/向前恢复)。
  • 统一手动恢复平台:基于第7篇transaction_log统一日志表,记录三种方案的事务xid、类型、状态。提供查询、详情、手动操作(TCC释放悬挂、消息重投、Saga补偿重试)。操作前验证权限并记录审计日志。

多角度追问

  1. “场景1如果团队没有TCC经验,能否用AT?”—— 可以,AT零侵入,但需加@GlobalLock防止脏读,且TPS可能略有下降,需压测验证。
  2. “场景2改为CDC发件箱后,Binlog格式如何管理?”—— 强制binlog_format=ROW,部署Debezium Connector监控其状态,P0告警。
  3. “场景3中,如果汇款涉及多个国家的银行系统,Saga如何编排?”—— 使用事件驱动或服务编排框架,每个银行作为远程参与者,提供正向和补偿接口,超时和重试策略需更长。

加分回答:全系统需统一traceId,贯穿日志和监控面板,实现全链路可观测。这一设计题串联系列全部7篇,展示从原理到选型、开发、监控、反模式规避、人工恢复的完整工程闭环,是高级工程师/架构师面试中的典型考题。


附录A:分布式事务选型速查表

方案一致性TPS上限延迟锁持有侵入性运维复杂度适用场景经典反模式关联篇号
XA强一致~500+10-50ms全局全程传统单体拆分,低并发强一致In-Doubt阻塞第1篇
AT强一致2000-3000+5-20msPhase1结束释放本地锁互联网中高并发,无侵入改造脏读窗口,undo_log膨胀第2篇
TCC强一致3000-5000+<5msTry结束高并发、有资源预留概念(金融)悬挂、空回滚、幂等缺失第3篇
Saga最终一致5000+分钟级(跨度)长事务,无法预留资源(旅游)补偿失败无人工介入第4篇
消息最终一致10000+1-3s或<100ms异步通知,解耦SENDING残留第5篇
CDC最终一致10000+<100ms异步解耦,已有Kafka生态Binlog格式错误第6篇

附录B:反模式排查速查表

反模式方案根因诊断命令/SQL修正操作关联监控指标关联篇号
In-Doubt阻塞XA协调者宕机xa recover; SHOW ENGINE INNODB STATUS;xa commit/rollback,重启协调者xa_indoubt_transactions_total第1、7篇
脏读窗口AT未加@GlobalLockSELECT * FROM undo_log WHERE xid=?添加@GlobalLock + SELECT FOR UPDATEat_dirty_read_count第2篇
undo_log膨胀AT未定期清理SELECT COUNT(*),SUM(data_length) FROM undo_log开启定时清理,手动删除过期undo_log_table_size第2、7篇
悬挂TCC防悬挂缺失SELECT * FROM tcc_fence_log WHERE business_id=?Try前检查CANCELLED,启用Seata TCCFencetcc_hanging_transactions_total第3篇
空回滚TCCCancel未处理空回滚SELECT * FROM tcc_fence_log WHERE business_id=?Cancel插入CANCELLED防悬挂tcc_empty_rollback_errors_total第3篇
幂等缺失TCCConfirm/Cancel无状态机SELECT business_id,action_type,COUNT(*)状态机乐观锁更新tcc_duplicate_confirm_total第3篇
补偿失败Saga无人工介入SELECT * FROM saga_transaction_log WHERE status='COMPENSATION_FAILED'手动重试/向前恢复,Admin后台saga_compensation_failed_total第4、7篇
SENDING残留消息发送线程宕机,无超时回退SELECT * FROM outbox WHERE status='SENDING' AND next_retry_time < NOW()-5 MIN重置为PENDINGoutbox_sending_stuck_total第5篇
Binlog错误CDC格式非ROWSHOW VARIABLES LIKE 'binlog_format'; GET /connectors/statusSET GLOBAL binlog_format=ROW,重启debezium_connector_status第6篇

延伸阅读

  • 《Designing Data-Intensive Applications》第7章(Transactions)、第9章(Consistency and Consensus)
  • 《Microservices Patterns》Saga相关章节
  • Seata官方文档:AT/TCC/Saga模式对比
  • RocketMQ官方文档:事务消息 vs 本地消息表
  • Debezium官方文档:Outbox Event Router + Troubleshooting

本文至此完整呈现了分布式事务六大方案的量化对比、选型决策树、九大反模式排查指南及监控联动体系,以及14道深度面试题。系列全八篇形成了从原理、实践、监控到决策与反模式的完整知识闭环,期望能为读者在分布式事务领域的工程实践提供扎实的参考。