用开奶茶店的方式,理解Seata的TCC模式

用开奶茶店的方式,理解Seata的TCC模式

大家好,我是Asthenia,今天咱们用开奶茶店的例子,聊聊分布式事务中的TCC模式。想象一下,你开了一家网红奶茶店,顾客下单后要同时扣减库存、生成订单、发放优惠券。这三个操作就像三个微服务,如何保证它们要么全部成功,要么全部回滚?这就是TCC要解决的难题。

一、TCC vs AT:自动挡和手动挡的区别

先说说AT模式,它像自动挡汽车:你只管踩油门(执行业务SQL),系统自动记录操作前后的数据镜像(就像行车记录仪),回滚时自动恢复。但遇到复杂业务(比如需要调用第三方接口),AT就力不从心了。

TCC模式则是手动挡:需要你亲自编写三个操作:

  • Try:准备原料(预留资源),比如冻结库存10杯奶茶
  • Confirm:正式制作(提交操作),实际扣减10杯
  • Cancel:退回原料(回滚操作),解冻10杯

就像顾客下单后,你先准备好奶茶原料(Try),等支付成功再制作(Confirm),如果取消订单就把原料放回冰柜(Cancel)。

二、设计TCC的三板斧

设计TCC服务就像设计奶茶制作流程:

1. Try阶段设计原则

  • 幂等性:重复点单不影响,用订单号做唯一标识
  • 空回滚防护:遇到未执行Try却收到Cancel时,要记录"空回滚标记"
  • 防悬挂:先执行Cancel再收到Try请求时,要拒绝执行

2. 典型代码结构

java 复制代码
@TwoPhaseBusinessAction(name = "reduceStock", commitMethod = "commit", rollbackMethod = "rollback")
public boolean prepare(BusinessActionContext context, 
                      @BusinessActionContextParameter(paramName = "sku") String sku,
                      @BusinessActionContextParameter(paramName = "count") int count) {
    // 冻结库存
    inventoryService.freeze(sku, count);
    return true;
}

public boolean commit(BusinessActionContext context) {
    // 实际扣减库存
    String sku = (String) context.getActionContext("sku");
    int count = (Integer) context.getActionContext("count");
    inventoryService.reduce(sku, count);
    return true;
}

public boolean rollback(BusinessActionContext context) {
    // 解冻库存
    String sku = (String) context.getActionContext("sku");
    int count = (Integer) context.getActionContext("count");
    inventoryService.unfreeze(sku, count);
    return true;
}

三、TCC实现原理拆解

我们通过奶茶订单的生命周期,看看TCC如何运转:

A. 注解驱动 - @TwoPhaseBusinessAction 就像奶茶配方标签:

java 复制代码
@TwoPhaseBusinessAction(name = "orderAction",
                       commitMethod = "confirmOrder",
                       rollbackMethod = "cancelOrder")
public void prepareOrder(...) {...}
  • name:动作名称(珍珠奶茶配方)
  • commitMethod:确认方法(制作奶茶)
  • rollbackMethod:回滚方法(退回原料)

B. 资源注册 - 备料登记 当应用启动时,GlobalTransactionScanner就像店长,扫描所有带@TwoPhaseBusinessAction的方法,把它们注册到TransactionManager(中央厨房管理系统):

java 复制代码
// 自动注册TCC资源
TCCResourceRegistry.getInstance()
    .registerResourceManager(new TCCResourceManager());

C. 事务发起 - 订单流水线

  1. 顾客下单:@GlobalTransactional标记的方法开始全局事务
java 复制代码
@GlobalTransactional
public void createOrder(OrderDTO order) {
    // 调用各个TCC服务
}
  1. 准备阶段:依次调用各服务的Try方法,就像准备奶茶原料
  2. 事务协调器(TC)记录事务状态,类似订单跟踪系统
  3. 成功时触发Confirm(制作奶茶),失败时触发Cancel(退回原料)

四、全局视角看TCC

整个过程就像奶茶店的全流程管理:

  1. 店长(TM)发起订单(全局事务)
  2. 各工位(RM)登记自己的任务(分支事务注册)
  3. 准备间完成备料(Try阶段)
  4. 中央厨房(TC)协调制作或取消

关键点在于事务上下文传递,就像订单小票在各部门流转。全局事务ID(XID)就是订单号,保证所有操作可追踪。

五、什么时候该用TCC?

  • 需要与外部系统交互(比如调用支付接口)
  • 业务补偿逻辑复杂(比如退单要恢复积分)
  • 对性能要求高(Try阶段只是资源预留)

但就像手动挡需要更多操作,TCC需要多写约3倍代码。对于简单场景,AT模式可能更合适。

总结一下,TCC模式就像精心设计的奶茶制作流程,通过明确的三个阶段,在分布式系统中实现了可靠的事务控制。理解它需要把握"预留-确认/取消"的核心思想,就像经营好一家奶茶店需要清晰的工序和应急方案。

六、TM/RM/TC的协作流程(厨房版)

角色定义

  • 顾客(TM):发起订单(全局事务)
  • 厨师(RM):处理具体菜品(分支事务)
  • 餐厅调度系统(TC):协调整个订单

文字时序图

ini 复制代码
1. [顾客] 下单酸菜鱼               -- TM发起@GlobalTransactional
   │
2. [调度系统] 生成订单号(XID)       -- TC创建全局事务
   │
3. [顾客] 通知后厨准备食材          -- TM调用RM的Try接口
   │   ├──> [厨师A] 备料酸菜鱼(Try)
   │   └──> [厨师B] 煮米饭(Try)
   │
4. [调度系统] 记录各菜品准备状态     -- TC注册分支事务
   │
5. 所有备料完成?                 -- TC检查Try结果
   │
6. 是 → [调度系统] 通知开始烹饪     -- TC触发Confirm
   │   ├──> [厨师A] 制作酸菜鱼(Confirm)
   │   └──> [厨师B] 蒸煮米饭(Confirm)
   │
   否 → [调度系统] 通知退回食材     -- TC触发Cancel
       ├──> [厨师A] 退还鱼肉(Cancel)
       └──> [厨师B] 倒掉生米(Cancel)

七、关键通信过程解析

1. 事务发起阶段(顾客点单)

java 复制代码
// TM端代码示例
@GlobalTransactional
public void placeOrder() {
    // 调用RM服务(会携带XID)
    dishService.prepare();
    riceService.prepare();
}
  • TM与TC通信:注册全局事务,获取XID(类似餐厅系统生成订单号)
  • 网络协议:TM通过Netty长连接与TC保持心跳

2. 分支注册阶段(厨师接单)

java 复制代码
// RM资源注册核心逻辑
public class DishTCCImpl implements TCCService {
    @Override
    @TwoPhaseBusinessAction(...)
    public boolean prepare() {
        // 自动向TC注册分支事务
        TransactionContextManager.getContext().addBranch();
    }
}
  • RM与TC通信:通过RPC上报分支状态(类似厨师报告备料进度)
  • 数据存储:TC将事务状态持久化到数据库(如MySQL)

3. 决议阶段(后厨协作)

  • 一阶段提交:TC异步批量处理Confirm/Cancel请求(类似调度系统批量通知厨师)
  • 异常处理:采用指数退避重试策略(如网络抖动时自动重试3次)

八、设计精妙之处

  1. 异步化设计:像餐厅的非阻塞式操作,TC处理请求不阻塞主流程
  2. 最终一致性:允许短暂状态不一致(就像允许备料和烹饪存在时间差)
  3. 故障隔离:单个厨师(RM)故障不影响整体(其他菜品仍可继续制作)

九、实际开发注意事项

  1. 网络超时 :设置合理的超时时间(就像规定厨师10分钟内必须响应)

    properties 复制代码
    # Seata客户端配置
    seata.tm.degrade-check-period=2000
    seata.tm.rollback-retry-timeout=120000
  2. 幂等设计 :给每个操作加唯一标识(类似给每道菜打上订单号)

    java 复制代码
    public boolean confirm(BusinessActionContext context) {
        String orderNo = context.getXid(); // 全局唯一标识
        if(processedOrders.contains(orderNo)) return true; 
    }
  3. 监控指标 :需要监控的关键指标(类似餐厅的看板系统)

    • TC端:全局事务成功率、平均处理时长
    • RM端:分支事务重试次数、资源冻结时间

通过这个视角,TCC就像一套精密的餐厅协作系统,每个组件各司其职,通过标准化的协议保证最终一致性。这种设计既保证了灵活性(允许各厨师自主操作),又通过中央调度系统(TC)确保了整体流程可控。

相关推荐
大脑经常闹风暴@小猿41 分钟前
1.1 go环境搭建及基本使用
开发语言·后端·golang
尚学教辅学习资料1 小时前
基于SpringBoot的美食分享平台+LW示例参考
spring boot·后端·美食
Vitalia3 小时前
从零开始学 Rust:基本概念——变量、数据类型、函数、控制流
开发语言·后端·rust
猎人everest6 小时前
SpringBoot应用开发入门
java·spring boot·后端
孤雪心殇11 小时前
简单易懂,解析Go语言中的Map
开发语言·数据结构·后端·golang·go
小突突突13 小时前
模拟实现Java中的计时器
java·开发语言·后端·java-ee
web1376560764313 小时前
Scala的宝藏库:探索常用的第三方库及其应用
开发语言·后端·scala
闲猫14 小时前
go 反射 interface{} 判断类型 获取值 设置值 指针才可以设置值
开发语言·后端·golang·反射
LUCIAZZZ14 小时前
EasyExcel快速入门
java·数据库·后端·mysql·spring·spring cloud·easyexcel
Asthenia041215 小时前
依托IOC容器提供的Bean生命周期,我们能在Bean中做些什么?又能测些什么?
后端