TCC模式实战:订单系统中的Try/Confirm/Cancel映射与一致性挑战
40
0
0
0
最近在重构公司的电商核心链路,TCC分布式事务模式又被提上了议程。说实话,TCC这三个字母念起来简单,但真要在订单、库存、积分、优惠券这几个核心系统里落地,里面的坑和细节真不少。
很多文章喜欢讲理论,咱们今天直接上场景:用户下单,系统需要依次完成:创建订单、扣除库存、扣除积分、发放优惠券。 这是一个典型的“强一致性”业务诉求,如果用最终一致性方案,库存超卖或者积分扣了订单没创建成功,用户投诉绝对会让你头疼。
下面拆解一下,TCC的三个阶段在这些业务操作中到底长什么样。
1. Try 阶段:资源预留与检查
Try阶段的核心不是直接执行业务,而是**“做戏做全套”的预备工作**。
- 订单系统 (Order Service):
- 并不是真的生成最终订单,而是创建一条状态为
Trying的订单记录。这行记录的存在,就是一种“占位”。 - 关键点: 检查用户状态是否正常,黑名单拦截等。
- 并不是真的生成最终订单,而是创建一条状态为
- 库存系统 (Stock Service):
- 这里最容易出错。不是直接扣减
quantity = quantity - 1。 - 而是执行冻结操作:
frozen = frozen + 1,可用库存保持不变。 - 目的: 防止Try阶段成功了,但后续Confirm失败,导致库存被“凭空”扣减。预留了资源,如果事务取消,再把冻结的加回来。
- 这里最容易出错。不是直接扣减
- 积分系统 (Point Service):
- 同理,不是直接扣积分,而是冻结积分。
usable_points = usable_points(不变),frozen_points = frozen_points + 100。
- 优惠券系统 (Coupon Service):
- 将优惠券状态置为
LOCKED(锁定),防止被其他人领走或使用。
- 将优惠券状态置为
总结: Try阶段干的都是“脏活累活”,主要是资源检查和预留,保证进入下一阶段前,资源是够用的且被锁住了。
2. Confirm 阶段:提交业务
如果Try阶段所有参与方都返回成功,TC(事务协调器)就会发起Confirm。
- 订单系统: 将
Trying状态的订单更新为Normal(正常) 或Paid(已支付)。 - 库存系统: 真正的扣减库存。
available = available - 1,同时将之前的frozen = frozen - 1。 - 积分系统: 真正的扣减积分。
usable = usable - 100,frozen = frozen - 100。 - 优惠券系统: 将优惠券状态从
LOCKED更新为USED(已使用)。
注意: Confirm阶段要求幂等性。如果网络抖动导致Confirm重试,不能重复扣款。
3. Cancel 阶段:回滚/补偿
如果Try阶段任意一个环节失败,或者Confirm阶段执行了一半挂了,TC会触发Cancel。
- 订单系统: 将
Trying状态的订单直接删除,或者标记为Cancelled。 - 库存系统: 解除冻结。
frozen = frozen - 1(把之前Try预留的还回去)。 - 积分系统: 解除冻结。
frozen = frozen - 100。 - 优惠券系统: 将
LOCKED状态的优惠券改回AVAILABLE(可用)。
注意: Cancel阶段同样要求幂等性,且要处理“空回滚”和“悬挂”问题(即Try没执行完,Cancel先执行了)。
可能遇到的数据一致性挑战
在实际落地中,TCC面临的挑战主要集中在以下几点:
悬空事务 (Hanging Transaction):
- 场景: Cancel接口因为网络拥堵,比Try请求先到达了事务协调器并执行了。此时数据库里没有Try阶段预留的数据,Cancel执行了空操作。等Try请求到达并执行成功后,事务就卡住了,资源一直被锁定。
- 对策: Cancel执行时,检查是否有对应的Try记录,如果没有,可能是空回滚,需要记录日志或插入一条空的Try记录以抵消。
幂等性控制 (Idempotency):
- 场景: Confirm阶段扣库存成功了,但因为网络超时,TC重试了Confirm请求,导致库存被扣了两次。
- 对策: 每个事务分支需要一个全局唯一的事务ID(XID)。在业务表中记录状态,或者利用Redis/数据库唯一索引去重。执行前先查一下是否已经处理过。
Try阶段的资源占用时间:
- 场景: 如果用户下单后一直不支付,Try阶段冻结的库存和积分会一直占用,导致其他用户买不到商品。
- 对策: 需要引入定时任务,扫描长时间处于
Trying状态的事务,自动触发Cancel操作,释放资源。
空回滚 (Null Rollback):
- 场景: Try阶段还没执行完,事务就失败了,直接执行Cancel。此时Try可能还没插入数据,Cancel操作找不到数据。
- 对策: Cancel时,如果发现没有对应的Try记录,直接返回成功即可,不需要报错。
TCC模式虽然代码侵入性强,需要开发大量的补偿接口,但它能保证强一致性,是处理资金、库存等核心资源最稳妥的方案之一。如果业务场景允许一定程度的数据延迟,其实消息队列+本地事务表的方案会更轻量。