标签: Seata

  • 深入 Seata AT 陷阱排查:@GlobalTransactional 滥用引发的全局锁竞争与 DB 连接池耗尽雪崩实战

    近期,核心交易链路在一次大促压测起步阶段便遭遇惨烈雪崩。订单服务接口 P99 延迟从 50ms 瞬间飙升至 8000ms,随后触发熔断,上游网关出现大量 503 HTTP 状态码。排查结论非常明确:业务研发在 C 端高并发下单接口上,极其粗暴地滥用了 Seata AT 模式(一种 2PC 的变体)。他们在包裹了三次跨网络 RPC 调用的上帝方法上打了一个 @GlobalTransactional 注解,导致本地 DB 连接被长时间挂起,加上 Seata TC(Transaction Coordinator)端的全局锁竞争,瞬间打穿了 HikariCP 连接池,最终引发微服务级联崩溃。

    分布式事务领域没有银弹,把 2PC 思想的框架当成本地 @Transactional 盲目使用,是对系统稳定性的严重破坏。

    案发现场:连接池耗尽与死锁报警

    排查过程中,订单服务最先弹出的核心告警是 DB 连接池被打满。查看应用日志,满屏都是 HikariCP 的获取连接超时异常:

    java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available, request timed out after 30005ms.
        at com.zaxxer.hikari.pool.HikariPool.createTimeoutException(HikariPool.java:695)
        at com.zaxxer.hikari.pool.HikariPool.getConnection(HikariPool.java:197)
    

    同时,MySQL 侧监控显示 Threads_running 飙升,通过 show engine innodb status\G 查看,发现大量事务处于 LOCK WAIT 状态。

    再去扒 Seata TC Server(我们部署的是集群模式,DB 存储)的日志,发现了大量全局锁获取失败的报错:

    [timeoutChecker_1] ERROR io.seata.server.coordinator.DefaultCore - Global lock acquire failed, xid: 192.168.1.10:8091:123456789, branchId: 987654321
    

    这一切的元凶,指向了订单服务中的一段“经典”代码:

    @GlobalTransactional(name = "create-order-tx", rollbackFor = Exception.class)
    @Transactional
    public Order createOrder(OrderDTO dto) {
        // 1. 本地 DB 扣减库存 (热点商品)
        inventoryMapper.decrease(dto.getSkuId(), dto.getQty());
        // 2. RPC 调用:扣减积分
        pointClient.decrease(dto.getUserId(), dto.getPoints());
        // 3. RPC 调用:核销优惠券
        couponClient.use(dto.getCouponId());
        // 4. 本地 DB 创建订单
        orderMapper.insert(order);
        return order;
    }
    

    致命的架构逻辑缺陷

    在微服务架构下,写出上面这种代码是不可原谅的。这不仅暴露了对分布式事务底层机制的无知,更是直接违背了“防御性编程”中关于资源控制的基本原则。

    1. 连接池被强行劫持(The I/O Trap) 在 Seata AT 模式的一阶段(Phase 1),业务数据和回滚日志(undo_log)必须在同一个本地事务中提交。当方法被 @Transactional 和 @GlobalTransactional 同时修饰时,Spring 会在方法一开始就从 HikariCP 借出一个 DB 连接。 随后发生的 pointClient 和 couponClient 是跨网络 RPC 调用。假设每个 RPC 耗时 200ms,这个 DB 连接就会被白白挂起至少 400ms,期间不执行任何实质性的 DB 读写。如果在 2000 QPS 的并发下,一个最大连接数为 200 的 HikariCP 连接池,不到 1 秒钟就会被彻底耗尽。

    2. 全局锁与本地行锁的双重绞杀(Lock Contention) Seata AT 为了保证隔离性(写隔离),在本地事务提交前,必须向 TC 注册分支事务并获取全局锁(Global Lock)。 在上述代码中,如果 dto.getSkuId() 是一个热点商品(例如秒杀),本地 MySQL 已经对该 SKU 所在的行加上了独占锁(X锁)。接着,应用带着这个状态去向 TC 申请该记录的全局锁。如果此时 RPC 调用出现哪怕一点网络抖动,或者 TC 端处理能力到达瓶颈,本地事务就会一直处于 WAIT 状态,死死抱着 MySQL 的行锁不放。这不仅会导致当前分布式事务超时,更会阻塞所有针对该热点 SKU 的普通更新操作,最终演变成 DB 层的锁雪崩。

    拆解与重构:抛弃 2PC,走向补偿与最终一致性

    对于 C 端高并发主链路,2PC(包括基于 2PC 演进的 AT 模式)永远是反模式。2PC 的同步阻塞特性决定了它只适合后台管理系统的低并发、强一致场景。

    面对这个烂摊子,我们在现场果断勒令研发团队进行重构,砍掉 Seata AT,改用 本地消息表(Outbox Pattern)+ 消息队列 的最终一致性方案,部分强依赖链路降级为 TCC(Try-Confirm-Cancel) 模式。

    方案一:RocketMQ 事务消息(最终一致性) 核心思路是把分布式事务降维打击成单机事务。 将扣减库存和创建订单放在一个本地事务中,利用 RocketMQ 的事务消息机制,确保本地 DB 提交和 MQ 消息发送的原子性。下游的积分和优惠券服务通过订阅 MQ 进行异步扣减。这种方式将跨服务的强一致解耦为最终一致,彻底释放了 DB 连接池压力。

    方案二:TCC 模式的工程取舍 如果某些业务场景强依赖严格的资源预留(例如不超卖、不超发),我们采用 TCC 模式。但在落地 TCC 时,必须在框架层死磕以下三个防御性设计:

    1. 防空回滚:Cancel 阶段被触发时,如果 Try 阶段由于网络延迟根本没执行,Cancel 逻辑必须识别并直接返回成功。我们在 TCC 事务控制表里增加一个状态字段,未找到 Try 记录的 Cancel 视为“空回滚”。

    2. 防悬挂:Cancel 接口比 Try 接口先到达(网络乱序)。应对方案是 Cancel 执行时不仅要判断是否空回滚,还要在控制表中插入一条“已回滚”记录。当迟到的 Try 请求到达时,发现该记录已经存在,则直接拒绝执行。

    3. 幂等性:Confirm 和 Cancel 极大概率会被重试,业务表必须引入 tx_id 唯一约束,确保重复请求被拦截。

    排查清单与避坑指南

    针对分布式事务(2PC / Saga / TCC)的落地实践,建议梳理以下排查清单,供排错和架构 Review 时对照:

    1. DB 连接泄漏审查:排查所有标注了分布式事务注解(如 @GlobalTransactional、@Compensable)的方法,严禁在事务上下文中包裹长耗时的外部 I/O 操作(RPC/HTTP 调用、耗时计算)。

    2. 锁超时配置对齐:必须确保 MySQL innodb_lock_wait_timeout > Seata client rm.lock-retry-timeout。否则会导致 MySQL 本地事务先于 Seata 释放锁,从而产生脏写(Dirty Write),破坏 undo_log,引发二阶段 Rollback 崩溃。

    3. TCC 防御性控制表校验:确认 TCC 参与者服务中是否实现了标准的事务状态表。必须同时包含 xid, branch_id, status 字段,并在 Try/Confirm/Cancel 阶段对其使用 select ... for update 或基于唯一索引进行状态机流转。

    4. TC 高可用与存储瓶颈:如果仍在使用 Seata AT,检查 TC Server 的存储模式。对于生产环境,坚决弃用 File 模式,改用 DB 或 Redis 模式,并监控 TC 端的 global_table 和 branch_table 的行数,防范僵尸事务堆积。

  • 深入 Seata AT 全局锁雪崩排查:2PC 滥用引发的 DB 连接池耗尽与 TCC 悬挂防线击穿实战

    某次核心链路压测排查中,接手了一个处于“植物人”状态的订单系统。现象极其惨烈:压测刚打到 500 QPS,订单库和库存库的 HikariCP 连接池瞬间 100% 耗尽,大量请求报 Connection timeout,99线从 30ms 飙升至 45s,系统完全夯死。 直接抛结论:这是典型的分布式事务滥用惨案。研发在面向 C 端的高并发链路上无脑贴 @GlobalTransactional 强行使用 Seata AT(2PC 变种)模式,导致底层资源被全局锁(Global Lock)和本地行锁双重绞杀。而在随后的紧急改造中,改用 TCC 模式却没做“防悬挂”和“空回滚”处理,导致网络抖动时出现大量脏数据。 高并发 C 端链路绝对不能碰强一致性的 2PC/AT 模式,老老实实用基于本地消息表或 MQ 的最终一致性(Saga/可靠消息),这是铁律。

    案发现场:被一把 @GlobalTransactional 瘫痪的数据库

    排查伊始,监控大屏上一片惨红。登录 DB 节点,直接 show processlist 和抓取 InnoDB 状态:

    -- 大量线程处于 Lock wait 状态
    mysql> SELECT * FROM information_schema.innodb_trx\G
    trx_state: LOCK WAIT
    trx_query: UPDATE inventory SET stock = stock - 1 WHERE sku_id = '10086'
    
    -- 查看锁等待
    mysql> SELECT * FROM sys.innodb_lock_waits;
    

    同时,应用层的日志疯狂输出 Seata TC(Transaction Coordinator)交互超时的报错:

    io.seata.core.exception.RmTransactionException: Response[ TransactionException[BranchRegister timeout] ]
    ...
    Caused by: io.seata.core.exception.TransactionException: Global lock acquire failed, xid: 192.168.1.10:8091:123456789
    

    原理还原:为什么 AT 模式会引发连接池雪崩? Seata AT 模式本质上是两阶段提交(2PC)的优化版。在 Phase 1,本地业务 SQL 执行完后,不会立刻提交数据库事务,而是要向 TC 申请全局锁(Global Lock)。 问题就出在这里:

    1. 事务 A 执行了 UPDATE inventory,拿到了 DB 的本地行锁。

    2. 事务 A 通过 RPC 去请求 TC 拿全局锁,此时网络抖动或 TC 负载高,RPC 阻塞。

    3. 事务 A 的数据库连接无法释放(因为事务没提交)。

    4. 事务 B、C、D 涌入,全部卡在等 DB 本地行锁上,迅速吃干整个 HikariCP 连接池。

    这种设计将 网络 I/O 延迟与数据库本地事务生命周期强绑定,在低频后台(B端)业务里用用也就罢了,拿到核心交易链路来跑,纯粹是嫌命长。

    踩坑续集:TCC 悬挂防线击穿实战

    在被勒令下线 AT 模式后,研发团队决定“重构”,引入 TCC(Try-Confirm-Cancel)模式。没过几天,客服开始反馈大量“库存扣了但订单取消”的客诉。

    我翻开他们的 TCC 补偿代码,差点没绷住:Cancel 方法里直接硬编码写了 UPDATE inventory SET stock = stock + 1。没有任何前置状态判断,完全把分布式网络当成了理想国。

    在分布式环境下,RPC 调用存在三大顽疾:丢包、延迟、乱序。这就必然导致 TCC 面临三个致命缺陷:

    1. 空回滚(Empty Rollback):Try 请求因为网络超时压根没到达参与者,但 TC 引擎认为超时了,直接触发 Cancel。参与者收到 Cancel 时,如果直接把库存 +1,凭空造出了资产。

    2. 幂等性失效(Idempotency):网络重试导致 Confirm 或 Cancel 被多次调用,库存被反复加减。

    3. 悬挂(Suspension):最隐蔽的杀手。Try 请求发出后遇到极大的网络延迟,TC 等不及了,触发了 Cancel(此时属于空回滚,防住了没造成危害)。但在 Cancel 执行完后,那个迟到的 Try 请求终于到了,并成功扣减了库存。此时全局事务早已结束,这个 Try 造成的改变将永远不会被回滚。这就是“悬挂”。

    把分布式事务当成本地 @Transactional 这种黑盒注解来用,缺乏对底层网络状态机的敬畏,出大事故是迟早的事。

    绝地反击:防御性 TCC 状态机落地实现

    要解决 TCC 的上述三大顽疾,千万不要在业务逻辑里用复杂的 if/else 去查业务表状态,极其容易出现并发竞态条件。 标准且优雅的做法是:建立一张独立的 TCC 事务控制表(tcc_tx_log),利用数据库的唯一索引(UK)和行锁来做防御。

    表结构核心字段:xid(全局事务ID), branch_id(分支事务ID), status(TRY, CONFIRM, CANCEL)。联合唯一索引:uk_xid_branch_id。

    实战防御伪代码/SQL:

    1. Try 阶段(防悬挂 + 防重复):

    // 尝试插入一条状态为 TRY 的记录
    int rows = jdbc.update("INSERT INTO tcc_tx_log (xid, branch_id, status) VALUES (?, ?, 'TRY')", xid, branch_id);
    // 如果抛出 DuplicateKeyException,说明两条路:
    // 1. Try 被重复执行(幂等拦截)
    // 2. Cancel 已经先执行过了(防悬挂拦截,Cancel 阶段会预埋一条 CANCEL 记录)
    if (exception) throw new TccException("并发重复执行或已发生悬挂");
    
    // 执行业务逻辑...
    

    2. Cancel 阶段(防空回滚 + 防悬挂 + 幂等):

    // 核心逻辑:Insert on duplicate key update
    // 如果记录不存在(说明 Try 没执行或者迟到了),直接插入一条 CANCEL 记录。
    // 这步极为关键:一旦插入了 CANCEL,后续迟到的 Try 就会在 Insert 时报主键冲突,彻底斩断悬挂!
    int rows = jdbc.update(
        "INSERT INTO tcc_tx_log (xid, branch_id, status) VALUES (?, ?, 'CANCEL') " +
        "ON DUPLICATE KEY UPDATE status = 'CANCEL' WHERE status = 'TRY'", 
        xid, branch_id
    );
    
    if (rows == 1 && inserted) {
        // 空回滚场景:记录不存在,直接插入了 CANCEL 状态。业务无需补偿,直接返回成功。
        return true;
    } else if (rows == 2 && updated) {
        // 正常回滚场景:把 TRY 更新成了 CANCEL。执行业务补偿逻辑。
        doBusinessRollback();
        return true;
    } else {
        // 幂等场景:状态已经是 CANCEL 了,直接返回成功。
        return true;
    }
    

    这套基于 DB 唯一索引的状态机,才是真正具备“防御性”的分布式事务工程实现。

    排查清单与避坑指南 (Troubleshooting Checklist)

    1. DB 连接池与事务超时监控:
    2. 在使用任何 2PC 方案时,务必对比监控 HikariCP Active Connections 与 TC Timeout 的指标关联性。若连接数飙升且慢查询中含大量等待 global_table 锁的操作,立即降级熔断。

    3. TCC 三防自检(防空回滚、防悬挂、幂等):

    4. Code Review 时直接搜索 Cancel 和 Confirm 方法,如果没有事务控制表(或类似 Redis Lua 状态机)的介入,直接打回重做。严禁裸写业务补偿逻辑。

    5. 架构选型纪律:

    6. C端高并发(如下单、秒杀):绝对禁用 2PC/AT/XA。只允许使用 Saga + 状态机 或 本地消息表 + MQ 最终一致性。
    7. 跨服务复杂长事务(如履约、资金清算):推荐使用 Saga 模式,按节点推进并做正向重试/逆向补偿。
    8. 内部后台低并发强一致(如配置同步、基础数据分配):可以使用 Seata AT 提升开发效率。
  • 深入剖析分布式事务的工程取舍:从 2PC 锁争用泥潭到 TCC 防悬挂实战

    核心结论:高并发核心链路严禁直接使用 XA/2PC 协议,其同步阻塞与全局锁定机制必然导致数据库连接池雪崩。Seata AT 模式虽通过一阶段提交缓解了长事务,但在热点行更新时,全依赖 TC 全局锁,极易造成 P99 延迟飙升。落地高并发分布式事务,最稳妥的解法是 TCC 或 Saga,并必须在底层辅以本地事务防悬挂控制表,实现极致的防御性编程。

    排查与重构高并发交易系统时,分布式事务永远是绕不开的雷区。很多人在架构选型时迷信各种中间件包装好的透明事务,却忽视了 CAP 定理下分布式事务的本质:通过牺牲可用性(锁阻塞)或牺牲一致性(最终一致补偿)来换取系统的流转。

    本文以 MySQL 8.0.32 和 Seata 1.6.1 为例,撕开分布式事务底层的工程细节,只谈实际落地时的痛点与防御。

    XA/2PC 的原罪:网络 RTT 与底层锁的致命耦合

    传统 XA 规范(2PC)的逻辑看似无懈可击:Prepare 阶段锁定资源,Commit/Rollback 阶段统一决断。但在实际微服务场景下,这是灾难的设计。

    当业务发起一次 XA 事务,MySQL 底层会执行 XA START -> SQL -> XA PREPARE。此时,InnoDB 引擎已经对涉及的数据行加上了排他锁(X Lock),并且这个锁的释放完全依赖于网络另一端 TM(Transaction Manager)的指令。

    你可以通过以下 SQL 在 MySQL 8.0+ 中观察到 XA 事务持有的锁阻塞情况:

    SELECT 
        p.trx_id, 
        p.trx_state, 
        p.trx_started, 
        l.lock_type, 
        l.lock_mode, 
        l.lock_data
    FROM performance_schema.data_locks l
    JOIN information_schema.innodb_trx p ON l.engine_transaction_id = p.trx_id
    WHERE p.trx_state = 'PREPARED';
    

    雪崩路径:

    1. 阶段一完成后,RM(数据库)持有行锁。

    2. TM 在阶段二由于网络抖动、GC 停顿或节点宕机,迟迟不发送 XA COMMIT。

    3. 其他并发请求试图访问该行数据,全部堆积在 innodb_lock_waits 中。

    4. 数据库连接池(如 HikariCP)迅速被占满,拖垮整个服务。

    这就是为什么在 C 端高并发核心链路(如库存扣减、资金转账)中,XA 协议属于绝对的禁区。

    为什么 Seata AT 模式在热点数据下会演变成性能灾难?

    为了解决 2PC 的长时间锁资源问题,Seata AT 模式应运而生。它的核心思想是:一阶段直接提交本地事务释放数据库锁,二阶段通过 undo_log 回滚。这听起来很完美,但它真的能抗住高并发吗?

    在某次大促压测中,我们发现扣减热点 SKU 库存时,TPS 始终卡在 300 左右,且 API 的 P99 延迟高达 3000ms+。抓取 Seata TC Server 的日志发现大量获取全局锁超时:

    [timeoutChecker_1] ERROR io.seata.core.lock.LockManager - Global lock wait timeout, xid: 192.168.1.10:8091:859392134, table: inventory, pk: 1001
    

    底层原理解析: Seata AT 为了防止脏写(Dirty Write),在本地事务提交前,必须向 TC(Transaction Coordinator)申请全局锁(Global Lock)。 如果两个并发请求同时修改同一行数据(例如热点 SKU id=1001):

    1. 事务 A 获取本地锁,修改数据。

    2. 事务 A 申请全局锁 inventory:1001,成功。A 提交本地事务,释放本地锁。

    3. 事务 B 获取本地锁,修改数据。

    4. 事务 B 申请全局锁 inventory:1001,失败,事务 A 尚未完成二阶段。

    5. 事务 B 必须等待,若超时则抛出 LockWaitTimeoutException,随后回滚本地事务。

    结论: Seata AT 只是把数据库的行锁争用,转移到了 Seata TC Server 的全局锁争用上。在热点行更新场景下,网络 RTT 被放大,性能瓶颈依然存在。AT 模式适合低并发的后台管理系统,绝不适合高并发交易链路。

    TCC 架构的防御性编程:空回滚、幂等与防悬挂实战

    既然底层锁不可靠,我们就必须走向应用层补偿事务,即 TCC(Try-Confirm-Cancel)或 Saga。 TCC 的 Try 阶段预留资源,Confirm 提交,Cancel 释放预留。但 TCC 落地的核心难点根本不是业务逻辑,而是分布式网络三大暗礁:网络重试导致的非幂等、空回滚、悬挂(Suspension)。

    • 空回滚:Try 请求因网络丢包未到达,TM 直接发起 Cancel。此时 Cancel 必须能够识别并成功返回。

    • 悬挂:Try 请求超时,TM 发起 Cancel 并执行成功。随后那个被网络延迟的 Try 请求终于到达了参与者。如果 Try 成功执行,预留的资源将永远无法被 Confirm 或 Cancel,造成数据死锁。

    最佳实践:基于本地控制表的 TCC 防御机制

    我们必须在业务数据库中建立一张 TCC 事务控制表,利用本地事务的 ACID 特性来抵抗分布式网络的混乱。

    CREATE TABLE `tcc_branch_control` (
        `xid` VARCHAR(128) NOT NULL COMMENT '全局事务ID',
        `branch_id` VARCHAR(128) NOT NULL COMMENT '分支事务ID',
        `status` TINYINT NOT NULL COMMENT '状态: 0-Try, 1-Confirm, 2-Cancel',
        `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
        `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
        PRIMARY KEY (`xid`, `branch_id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
    

    Try 阶段的防御代码逻辑:

    在 Try 方法中,我们将业务 SQL 与插入控制表包裹在同一个本地事务中。

    @Transactional(rollbackFor = Exception.class)
    public boolean tryDeduct(String xid, String branchId, String sku, int count) {
        // 1. 防悬挂与幂等检查:尝试插入 Try 记录
        // 如果插入失败(主键冲突),说明 Try 已执行(需处理幂等),或者 Cancel 已经执行(发生悬挂)
        int insertCount = tccControlMapper.insertIgnore(xid, branchId, 0);
        if (insertCount == 0) {
            TccControl record = tccControlMapper.select(xid, branchId);
            if (record.getStatus() == 2) {
                log.warn("防悬挂拦截: Cancel已执行, 丢弃迟到的Try请求, xid: {}", xid);
                return false; 
            }
            log.info("Try 幂等放行, xid: {}", xid);
            return true;
        }
    
        // 2. 正常执行 Try 业务逻辑 (如: 冻结库存)
        inventoryMapper.freeze(sku, count);
        return true;
    }
    

    Cancel 阶段的防御代码逻辑:

    @Transactional(rollbackFor = Exception.class)
    public boolean cancelDeduct(String xid, String branchId, String sku, int count) {
        // 1. 尝试插入 Cancel 记录 (防御空回滚)
        // 如果之前没有 Try 过,这里会插入成功,状态为 2 (Cancel)。
        // 这同时阻断了后续迟到的 Try (防悬挂)。
        int insertCount = tccControlMapper.insertIgnore(xid, branchId, 2);
        if (insertCount > 0) {
            log.info("空回滚执行: 记录Cancel状态, 拦截后续Try, xid: {}", xid);
            return true;
        }
    
        // 2. 检查当前状态
        TccControl record = tccControlMapper.select(xid, branchId);
        if (record.getStatus() == 2) {
            log.info("Cancel 幂等放行, xid: {}", xid);
            return true;
        }
    
        // 3. 执行资源释放,并更新状态为 Cancel
        inventoryMapper.unfreeze(sku, count);
        tccControlMapper.updateStatus(xid, branchId, 2);
        return true;
    }
    

    通过这一张表和一个 INSERT IGNORE 指令,我们在数据库引擎层面完美防范了所有由于网络乱序引发的事务状态异常。

    Saga 模式的取舍:隔离性的彻底放弃

    当你的分布式事务跨越了第三方系统(如调用外部银行接口),你无法要求第三方提供 Try 接口预留资源,此时 TCC 不适用,只能退化为 Saga 模式。

    Saga 也是两阶段:一阶段直接执行正向业务(如直接入账),二阶段执行补偿业务(如扣减入账)。 它的最大缺陷是缺乏隔离性。在正向业务执行完,补偿业务尚未执行的这段时间窗口内,其他事务可能会读取甚至修改这部分数据(脏读、脏写)。

    Saga 防治脏写的底线: 如果采用 Saga,必须引入乐观锁(版本号机制)或状态机。一旦补偿阶段发现数据的版本号被其他事务推进过,绝对不能强行执行回滚逻辑,必须立即阻断补偿链路,抛出异常,转入人工对账异常队列表。自动化的尽头是人工,这是容灾兜底的最后防线。

    常见问题 (FAQ)

    Q1:在 TCC 模式下,如果 Confirm 或 Cancel 阶段执行失败(比如数据库临时宕机),应该怎么处理? A: TCC 的设计前提是 Confirm 和 Cancel 必须最终成功。如果阶段二失败,TM(Transaction Manager)会不断重试。工程实现上,必须保证阶段二的绝对幂等性。如果重试超过一定阈值(如重试 5 次依然报错),通常意味着出现了底层硬故障(如坏块或长期的依赖宕机)。此时 TM 会记录异常日志,触发告警,转由人工介入。绝对不要在阶段二返回业务层面的错误。

    Q2:Saga 模式执行补偿逻辑时,发现数据已经被用户修改过了(脏写),如何进行补偿? A: 这是 Saga 的经典痛点。在设计 Saga 时,必须对被操作的数据加上状态锁或语义锁。例如订单状态变更为“发货中”,此时如果触发补偿,发现状态已经是“已收货”,就不应该直接执行逆向逻辑。一旦检测到脏写(通过乐观锁版本号或状态机流转规则拦截),系统应该停止自动补偿,触发风控或异常对账流程,由运营人员判断是否需要人工冲正。

    Q3:Seata Server (TC) 如果发生 OOM 或者宕机,对正在运行的业务有什么影响? A: 以 Seata 1.6.1 为例,TC 本身无状态,其事务数据存储在 MySQL 或 Redis 中。如果 TC 宕机,客户端的发起的全局事务将无法注册或提交,业务接口会大量抛出 TransactionException,导致新事务完全中断(可用性受损)。对于已经进入二阶段的事务,待 TC 恢复后,会从数据库读取处于 COMMITTING 或 ROLLBACKING 状态的会话,继续下发二阶段指令。监控上会观察到活跃事务数(Active Transactions)剧增。