分布式事务最佳实践:TCC空回滚、Saga补偿风暴与选型建议

简介: 讲述分布式事务从2PC到TCC再到Saga的踩坑经历,解析强一致性与最终一致性的取舍、TCC的空回滚悬挂幂等问题、Saga的补偿风暴,附选型建议和避坑清单

大家好,我是数据库小学妹 👋

上个月接手了一个跨库转账的需求。当时想,不就是保证多个数据库操作要么全成功要么全失败嘛,能有多难。

结果第一个坑就栽在2PC上。用了一个开源框架,测试环境跑得好好的,一上生产就卡。高峰期响应时间飙到几秒,用户投诉了好几轮。排查了半天,发现是2PC同步阻塞的问题。后来换成TCC,又踩了空回滚的坑,账差点对不上。折腾了好几周,才算把这事理清楚。

今天把这些踩坑经历整理出来,希望能帮你少走弯路少踩坑。

一致性到底是什么

分布式事务要解决的问题:多个服务、多个数据库之间,怎么让数据保持一致。

强一致性,任何时刻所有节点的数据都一样。听着挺好,但实现成本高,性能差。

最终一致性,允许短暂不一致,但最终所有节点会达成一致。成本低,性能好。

大部分业务不需要强一致性。下单后库存扣减和订单创建,中间差几毫秒,用户感知不到。

但转账这种场景,钱不能少不能多,就得上更强的保证。

2PC和3PC

2PC是最经典的分布式事务协议,分两个阶段。

准备阶段:协调者问所有参与者"能不能提交",参与者执行事务但不提交,返回YES或NO。

提交阶段:所有参与者都返回YES,协调者发"提交"命令;否则发"回滚"命令。

这几个问题我在生产环境都遇到过:

同步阻塞。 Prepare阶段所有参与者都锁着资源。我查了监控,高峰期锁持有时间达到500ms。业务操作本身才10ms,剩下全是等锁。并发一上来,请求全排着队,响应时间飙到几秒。

单点故障。 协调者挂了,参与者就卡在Prepare阶段。我遇到过一次协调者OOM,整个分布式事务卡了十几分钟。当时排查了好久才发现是协调者的问题。

脑裂。 网络分区时,协调者可能给部分参与者发提交,部分发回滚。数据就乱了。

3PC加了超时机制,参与者等太久会自动提交。但还是解决不了脑裂问题。

实际项目中很少直接用2PC/3PC,更多用TCC或Saga。

TCC模式

TCC把分布式事务拆成三个阶段:

Try做资源预留。转账场景,先把A账户100块冻结,不是直接扣掉。

Confirm确认提交。所有Try成功后,执行实际操作。把冻结的100块从A转到B。

Cancel取消回滚。任一Try失败,把冻结的100块解冻。

TCC有三个坑,我都踩过。

空回滚

Cancel执行了,但Try根本没执行过。

我遇到的情况是:Try请求超时了,调用方以为失败就调Cancel。但Try其实成功了,钱已经冻结了。Cancel执行的时候,把没冻结的钱给"解冻"了,账对不上。

排查时我先以为是网络重试导致的双重扣款。看了半天日志才发现,Cancel有记录,但Try没有对应的冻结记录。才反应过来——这是空回滚。

怎么解:Cancel执行前,先查Try有没有执行过。加一张事务记录表,Try成功就写一条。

CREATE TABLE tcc_transaction (
  tx_id VARCHAR(64) PRIMARY KEY COMMENT '事务ID',
  status VARCHAR(16) NOT NULL COMMENT 'INIT/TRIED/CONFIRMED/CANCELLED',
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

Try执行时先插入记录,Cancel执行时先查记录:

// Try:先标记,再冻结
public void try(String txId) {
   
    insert(txId, "INIT");
    freezeAmount(100);
    updateStatus(txId, "TRIED");
}

// Cancel:先检查Try是否执行过
public void cancel(String txId) {
   
    String status = queryStatus(txId);
    if (status == null) {
   
        // Try没执行过,标记取消即可,不用解冻
        insert(txId, "CANCELLED");
        return;
    }
    unfreezeAmount(100);
    updateStatus(txId, "CANCELLED");
}

悬挂

Cancel执行完了,Try才到。

比如网络抖动,Cancel先执行,资源释放了。然后Try到了,资源又被预留了。但没人来Confirm或Cancel,资源就一直挂着。

怎么解:Try执行前,检查Cancel有没有执行过。

public void try(String txId) {
   
    String status = queryStatus(txId);
    if ("CANCELLED".equals(status)) {
   
        return; // Cancel已执行,Try不再执行
    }
    // 正常执行
    insert(txId, "INIT");
    freezeAmount(100);
    updateStatus(txId, "TRIED");
}

幂等

Confirm或Cancel可能被重复调用。不处理的话,重试时钱可能多扣或多加。

怎么解:执行前检查状态,做过了就跳过。

Saga模式

另一种方案是Saga。把长事务拆成多个本地事务,每个都有对应的补偿操作。

下单流程为例:创建订单→扣减库存→扣减余额。失败就反向补偿:恢复余额→恢复库存→取消订单。

Saga分两种。编排式没有中央协调者,服务通过事件通信。协调式有中央协调者,统一调度。

Saga踩坑也不少。

最头疼的是补偿风暴。步骤越多,补偿链越长。我那个转账场景还好,只有两步。后来有个下单流程涉及5个服务,中间步骤失败了,前面4个都要补偿,整个回滚跑了十几秒。

还有数据可见性的问题。中间步骤完成后数据已经可见了。用户看到"下单成功",结果后面扣款失败,订单被取消。体验很差。

另外补偿操作本身也可能失败,需要重试。但重试就要保证幂等,不然会出问题。

补偿幂等我踩过坑。有一次网络重试,库存恢复了两次,账对不上。后来每条补偿SQL都加了状态检查:

-- 补偿前先检查状态
UPDATE inventory
SET stock = stock + 1, status = 'COMPENSATED'
WHERE order_id = 'xxx' AND status != 'COMPENSATED';

TCC和Saga怎么选

我现在的理解:

维度 TCC Saga
开发成本 高,每个操作都要写Try/Confirm/Cancel 中,每个操作写正向+补偿
资源锁 Try阶段锁定,时间短 不锁定,中间数据可见
一致性 最终一致(通过Confirm/Cancel保证) 最终一致(通过补偿保证)
适用场景 转账、支付等强一致要求 订单、物流等长流程
踩坑重点 空回滚、悬挂 补偿风暴、数据可见性

实际项目中,很多场景用消息队列加本地事务表就够了。能用简单方案解决的,别上复杂框架。

避坑清单

  1. 别一上来就上分布式事务。先想想能不能用本地事务解决,大部分场景不需要。
  2. TCC一定要处理空回滚、悬挂、幂等。这三个坑我都踩过,不处理账迟早对不上。
  3. Saga补偿操作必须加幂等检查。我之前没加,网络重试导致库存恢复了两次。
  4. 框架选型要慎重。AT模式看着简单,但锁竞争激烈,高并发场景扛不住。
  5. 监控必须到位。分布式事务出了问题排查很麻烦,建议用链路追踪把调用链记下来。
  6. 降级方案提前准备好。我有一次Cancel操作卡了,第三方接口慢,整个事务卡了20分钟才发现。后来加了超时控制和降级方案,才没再出问题。

你们项目里用的什么分布式事务方案?评论区聊聊,有踩过坑的也欢迎分享。

我是数据库小学妹,咱们下篇见 👋

相关文章
|
算法 Java
jvm性能调优 - 15JVM的老年代垃圾回收器CMS的缺点
jvm性能调优 - 15JVM的老年代垃圾回收器CMS的缺点
505 0
|
3月前
|
消息中间件 NoSQL 数据库
分库分表后数据不一致?3种分布式事务方案,帮你彻底解决“钱货不等”难题
本文由“数据库小学妹”详解分布式事务核心难题:分库分表后如何保障跨库数据一致性。涵盖TCC、消息队列(最终一致性)、2PC等方案对比,强调互联网场景首选“MQ+幂等+本地消息表”,并指出避坑要点(重复消费、消息丢失、悬挂问题)。
|
3月前
|
算法 关系型数据库 MySQL
分库分表:新手必踩的3大深坑与避坑清单
本文是MySQL分库分表实战避坑指南,聚焦ShardingSphere场景,直击主键冲突、跨库查询慢、扩容迁移难三大高频痛点,详解雪花算法、分片键路由、双写迁移等生产级解决方案,助你安全落地分布式数据库架构。
|
消息中间件 缓存 监控
系统稳定性建设实践总结
2020年,注定是个不平凡的一年。疫情的蔓延打乱了大家既定的原有的计划,同时也催生了一些在线业务办理能力的应用诉求,作为技术同学,需要在短时间内快速支持建设系统能力并保障其运行系统稳定性。恰逢年终月份,正好梳理总结下自己的系统稳定性建设经验和思考。
系统稳定性建设实践总结
|
Web App开发 iOS开发
无法安装此app,因为无法验证其完整性 ,解决方案
无法安装此app,因为无法验证其完整性 ,解决方案
|
3月前
|
缓存 监控 NoSQL
【Redis】Redis缓存三大核心问题:缓存穿透 / 击穿 / 雪崩(原因 + 解决方案)
本文系统解析Redis缓存三大高危问题:**穿透**(查不存在数据)、**击穿**(热点Key过期瞬间并发压库)、**雪崩**(大量Key集中失效或集群宕机)。深入剖析根因,提供分层防护方案——布隆过滤器+参数校验防穿透、永不过期+本地缓存防击穿、过期打散+高可用架构防雪崩,并强调全链路兜底与生产避坑要点。
|
3月前
|
SQL 存储 关系型数据库
【MySQL】《MySQL索引 与 慢SQL优化 面试问答清单》(附《思维导图》)
本文是面向MySQL开发者与DBA的索引与慢SQL优化实战指南,涵盖B+树原理、聚簇/覆盖索引、EXPLAIN深度解读、四大阶段优化(发现→分析→索引→重构)及电商订单/多表联查真实案例,含可打印速查表,助你将慢查询从3秒优化至0.005秒。
【MySQL】《MySQL索引 与 慢SQL优化 面试问答清单》(附《思维导图》)
|
2月前
|
人工智能 NoSQL Java
【Spring AI Alibaba 实战】大模型也有“金鱼记忆”?详解短时记忆(Chat Memory)核心原理与生产级实践
在使用 Spring AI Alibaba 开发大模型应用时,你是否发现模型总是“记不住”上一轮对话的内容?这并非模型智商问题,而是缺少了“短时记忆”机制。本文将深入剖析 Spring AI Alibaba 中 Chat Memory 的设计哲学,从内存存储到 Redis 持久化,手把手带你构建具备上下文感知能力的智能应用,并附上生产环境的避坑指南。
|
8月前
|
Linux 编译器 C语言
Linux 麒麟系统安装 gcc-7.3.0 rpm 包步骤
本文介绍如何在Linux系统中手动安装GCC 7.3.0的RPM包。包含下载路径、终端操作、目录切换、依赖处理及版本验证步骤,推荐使用yum安装以自动解决依赖问题,并提供常见问题解决方案,确保顺利安装GCC编译器。
|
自然语言处理 机器人 Go
【飞书ChatGPT机器人】飞书接入ChatGPT,打造智能问答助手
【飞书ChatGPT机器人】飞书接入ChatGPT,打造智能问答助手
1450 0