tcc的try阶段必须写冻结表而非改主表,因为try是资源预占而非最终扣减;直接改主表会破坏数据实时可见性、无法区分“待确认”与“已确认”状态,且导致confirm重试重复扣减或cancel时无法准确回滚。

Go里TCC的Try阶段为什么必须写冻结表而不是改主表
因为Try阶段不是最终扣减,而是资源预占;直接改主表会破坏业务数据的实时可见性,且无法区分“已确认”和“待确认”状态。
常见错误是把tryReserve实现成直接UPDATE库存主表的quantity -= n,结果Confirm重试时又扣一次,或者Cancel时不知道该加回多少。
- 正确做法:建一张
inventory_freeze表,字段含saga_id、product_id、quantity、status,联合唯一索引为(saga_id, product_id) - Try只INSERT这条冻结记录,并UPDATE主表
available_stock(不是total_stock) - Confirm查
inventory_freeze中status = 'TRYING'的记录,转为'CONFIRMED'并真正扣减total_stock - Cancel同理,只操作冻结记录,不碰
total_stock,避免补偿逻辑污染主数据
Saga补偿函数为什么不能写成deleteOrder
因为订单可能已被下游服务引用(如物流单号已生成、发票已开出),物理删除会引发外键异常或状态断裂,导致事务链彻底不可逆。
典型现象:cancelOrder返回成功,但物流系统仍按原订单发货,因为它的状态同步依赖订单表的status字段而非是否存在。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 正向操作是
createOrder,对应补偿必须是update order set status = 'CANCELLED' where id = ? and status = 'CREATED' - 所有补偿函数必须幂等:重复执行
cancelOrder不应改变最终状态 - 补偿动作本身也要可重试——比如退款失败后,需通过MQ重投补偿命令,而不是靠定时任务轮询
- 推荐用状态机(如
github.com/looplab/fsm)管理当前Saga步骤,避免靠DB字段值判断进度
Go中Saga编排器怎么避免本地事务和远程调用不同步
关键在于“本地事务内落库 + 异步发消息”,不能把DB提交和HTTP调用包在一个tx.Commit()里——网络超时会导致事务卡住或消息丢失。
错误写法:db.Transaction(func(tx *gorm.DB) error { tx.Create(&order); http.Post(...); return nil }),一旦http.Post失败,订单已入库却没触发下一步。
- 正确流程:在本地事务中插入
saga_log记录(含step、status='PENDING'、saga_id),再提交;然后由后台goroutine监听该表或发Kafka事件 - 每个Saga步骤的执行入口必须带
context.Context,透传saga_id和step_id,方便日志串联与人工干预 - 远程调用失败时,更新
saga_log.status = 'FAILED',触发补偿调度器拉起反向补偿链 - 别用GORM的
AfterCreatehook自动发MQ——hook在事务内执行,同样面临同步阻塞问题
TCC Confirm卡住超时怎么办
Confirm本应是轻量原子操作,但如果它包含远程调用、复杂计算或未加锁的DB更新,就极易超时,进而导致整个TCC事务悬挂。
现象:Try成功,Confirm调用发出但无响应,协调器重试多次后放弃,Cancel也因网络问题失败,冻结资源长期滞留。
- Confirm逻辑必须限定为:查冻结记录 → 更新主表 → 删除/标记冻结记录,三步都在一个DB事务内完成
- Confirm接口要支持
idempotent key参数(如saga_id),避免重试时重复执行 - HTTP handler里别直接执行Confirm,先写
saga_confirm_task表,再由worker轮询执行,超时自动标记为NEED_MANUAL - Cancel也一样——它不是Confirm的逆操作,而是基于冻结记录是否存在做决策,不是查主表余额
order_id)+ 幂等键(如saga_id),否则重试机制一开,数据就乱了。golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










