标签: 高并发

  • 深入 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):网络重试导致 ConfirmCancel 被多次调用,库存被反复加减。

    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 ConnectionsTC Timeout 的指标关联性。若连接数飙升且慢查询中含大量等待 global_table 锁的操作,立即降级熔断。

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

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

    5. 架构选型纪律

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