分布式事务工程实战:Saga 编排与 TCC 协议在微服务架构中的生产落地
在微服务架构中,数据一致性是最棘手的技术挑战之一。本文深入拆解 Saga 与 TCC 两大主流分布式事务模式,从原理到代码,从设计陷阱到生产优化,给出可直接落地的工程实践方案。
一、为什么分布式事务如此棘手
单体架构中,一个 @Transactional 就能搞定的事务,在微服务拆分后变成了跨网络的分布式难题。银行转账场景:A 账户扣款成功,B 入账时网络超时,钱"消失了"——这就是典型的分布式事务不一致。
传统两阶段提交(2PC)在理论上能保证强一致性,但在生产环境中几乎不可用:
- 同步阻塞:Coordinator 必须等待所有 Participant 响应,RT 急剧上升
- 单点故障:Coordinator 宕机导致参与者持有锁无限等待
- 脑裂风险:网络分区时部分节点 Commit 另一部分 Abort
- 数据不一致:第二阶段 Commit 消息丢失导致永久不一致
因此,业界主流转向最终一致性方案——Saga 和 TCC。
二、Saga 模式:长事务的拆解哲学
2.1 核心思想
Saga 将一个大事务拆分为一系列本地事务 T1, T2, ..., Tn,每个本地事务都有对应的补偿事务 C1, C2, ..., Cn。如果 T3 失败,则逆序执行 C2、C1 进行回滚。
关键公式:T1 → T2 → ... → Tn(正向);Cn → ... → C2 → C1(补偿)
2.2 两种实现架构
编排式(Orchestration)
由一个中央协调者(Saga Orchestrator)控制事务流程,指挥各服务执行本地事务或补偿操作。
// Saga 编排器核心结构
type SagaOrchestrator struct {
steps []SagaStep
currentIdx int
status SagaStatus
compensations []CompensationRecord
}
type SagaStep struct {
Service string // 目标服务
Action func() error // 正向操作
Compensate func() error // 补偿操作
ActionURL string // HTTP 端点
CancelURL string // 补偿端点
}
// 执行 Saga
func (s *SagaOrchestrator) Execute(ctx context.Context) error {
for i, step := range s.steps {
s.currentIdx = i
s.status = Executing
if err := s.executeStep(ctx, step); err != nil {
s.status = Failed
// 补偿已完成的步骤
return s.compensate(ctx, i-1)
}
}
s.status = Completed
return nil
}
// 逆序补偿
func (s *SagaOrchestrator) compensations(ctx context.Context, lastCompleted int) error {
for i := lastCompleted; i >= 0; i-- {
step := s.steps[i]
if err := s.executeCompensation(ctx, step); err != nil {
// 补偿失败需要人工介入 + 告警
s.status = CompensationFailed
return fmt.Errorf("compensation failed at step %d: %v", i, err)
}
}
s.status = Compensated
return nil
}
优点:流程清晰、调试方便、适合复杂业务流。
缺点:Orchestrator 成为逻辑上帝类,所有业务逻辑集中于一处。
协同式(Choreography)
无中心协调者,每个服务完成本地事务后发布事件,其他服务监听事件触发下一个事务。
// 协同式 saga:事件驱动
// 订单服务
func (s *OrderService) CreateOrder(ctx context.Context, req CreateOrderRequest) error {
// 1. 创建订单(本地事务)
orderID, err := s.orderRepo.Create(ctx, req)
if err != nil {
return err
}
// 2. 发布"订单已创建"事件
event := OrderCreatedEvent{
OrderID: orderID,
UserID: req.UserID,
TotalPrice: req.TotalPrice,
Items: req.Items,
}
return s.eventBus.Publish(ctx, "order.created", event)
}
// 支付服务:监听订单创建事件
func (s *PaymentService) HandleOrderCreated(ctx context.Context, event OrderCreatedEvent) error {
// 尝试扣款
err := s.paymentGateway.Charge(ctx, event.UserID, event.TotalPrice)
if err != nil {
// 发布支付失败事件,触发补偿
return s.eventBus.Publish(ctx, "payment.failed", PaymentFailedEvent{
OrderID: event.OrderID,
Reason: err.Error(),
})
}
return s.eventBus.Publish(ctx, "payment.success", PaymentSuccessEvent{
OrderID: event.OrderID,
PaidAmount: event.TotalPrice,
})
}
优点:去中心化、服务松耦合、扩展性好。
缺点:流程隐含在事件流中难以追踪、调试困难、循环依赖风险。
2.3 Saga 设计的三大陷阱
陷阱一:补偿事务未考虑数据可见性
假设 T1 提交了订单创建,补偿 C1 执行"订单取消"。但在 T2(支付)执行期间,用户看到订单状态为"已完成",随后却变成了"已取消"——用户体感极差。
解决方案:引入中间状态(Processing/Cancelling),补偿时不直接翻转状态,而是设置 Cancelling,前端轮询时返回"处理中"。
陷阱二:补偿操作幂等性缺失
网络超时导致补偿重试,若 C1 将库存加两次,数据就错了。
// 补偿操作的幂等控制
func (s *InventoryService) CompensateReserve(ctx context.Context, txID string, items []Item) error {
// 是否已补偿过
if done, _ := s.compensationLog.IsCompensated(ctx, txID); done {
return nil // 已处理过,幂等返回
}
// 记录补偿开始(防重入)
if err := s.compensationLog.MarkCompensating(ctx, txID); err != nil {
return err // 另一个线程正在补偿
}
// 执行库存释放
for _, item := range items {
s.inventoryRepo.Increase(ctx, item.SKU, item.Quantity)
}
return s.compensationLog.MarkCompensated(ctx, txID)
}
陷阱三:Saga 并发冲突
两个 Saga 实例同时操作同一商品库存,A 的 C1 补偿后 B 的 T2 又成功了,最终数据不一致。
解决方案:在 Saga 层面加乐观锁或使用业务时间戳校验(类似 Vector Clock)。
三、TCC 模式:三阶段精细控制
3.1 Try-Confirm-Cancel 哲学
TCC 通过三个阶段显式控制资源预留与确认:
- Try:预留资源(检查 + 冻结),不是实际提交
- Confirm:确认提交,真正完成业务
- Cancel:取消预留,释放冻结资源
与 Saga 的区别:TCC 的 Try 阶段是"预占用",Confirm/Cancel 才是真正的提交/回滚。Saga 的每个 T 是真实提交,补偿是逆向回滚。
3.2 TCC 协议实现
// TCC 接口定义
type TCCParticipant interface {
Try(ctx context.Context, bizID string, params interface{}) error
Confirm(ctx context.Context, bizID string) error
Cancel(ctx context.Context, bizID string) error
}
// 库存服务的 TCC 实现
type InventoryTCC struct {
repo InventoryRepository
logger *TCCLog // TCC 事务日志
}
// Try:检查库存并冻结
func (s *InventoryTCC) Try(ctx context.Context, bizID string, params interface{}) error {
p := params.(ReserveParams)
// 幂等检查——同一 bizID 是否已处理过
state, _ := s.logger.GetState(ctx, bizID, "inventory")
if state != "" {
return nil // 已 Try 过
}
// 库存检查
available := s.repo.GetAvailable(ctx, p.SKU)
if available < p.Quantity {
return ErrInsufficientInventory
}
// 冻结库存(可用减少、冻结增加、实际未扣减)
s.repo.Freeze(ctx, p.SKU, p.Quantity)
// 记录 Try 状态
return s.logger.TryRecorded(ctx, bizID, "inventory")
}
// Confirm:真正扣减冻结的库存
func (s *InventoryTCC) Confirm(ctx context.Context, bizID string) error {
// 是否已 Confirm
if state, _ := s.logger.GetState(ctx, bizID, "inventory"); state == "confirmed" {
return nil
}
// 确认是否已 Try
if state, _ := s.logger.GetState(ctx, bizID, "inventory"); state != "tried" {
return ErrInvalidState // 需要 Cancel 或处于不一致状态
}
// 将冻结转为真实扣减
s.repo.ConfirmDeduction(ctx, bizID)
return s.logger.ConfirmRecorded(ctx, bizID, "inventory")
}
// Cancel:释放冻结库存
func (s *InventoryTCC) Cancel(ctx context.Context, bizID string) error {
state, _ := s.logger.GetState(ctx, bizID, "inventory")
// 空回滚防护——如果 Try 未执行就收到 Cancel
if state == "" {
// 记录 Cancel 防止后续 Try 生效
s.logger.CancelRecorded(ctx, bizID, "inventory")
return nil
}
if state == "canceled" {
return nil // 幂等
}
if state == "confirmed" {
// 已 Confirm 的不能 Cancel
return ErrAlreadyConfirmed
}
// 释放冻结库存
s.repo.ReleaseFreeze(ctx, bizID)
return s.logger.CancelRecorded(ctx, bizID, "inventory")
}
3.3 TCC 三大工程难题
难题一:空回滚(Empty Rollback)
Try 阶段因网络问题超时,TM 认为失败触发 Cancel,但实际 Try 还在执行中。Cancel 先执行造成了"空回滚",随后 Try 执行成功——资源被永久冻结。
解决:Cancel 阶段插入一条"取消记录",Try 时检查该记录是否存在。如存在则拒绝执行。
难题二:悬挂(Suspend)
Try 因网络慢未执行,Cancel 先执行成功(空回滚),随后 Try 到达——正常执行后永远无法 Confirm,资源被悬挂。
解决:Try 时检查已有 Cancel 记录,若存在则放弃 Try 并记录异常日志。
难题三:幂等
Confirm/Cancel 因网络重试多次调用,可能导致重复扣款或重复释放。
解决:状态机校验,每次操作前检查当前状态,"tried" 只能转 "confirmed" 或 "canceled","confirmed" 不可再变。
四、Saga vs TCC:何时选用
| 维度 | Saga | TCC |
|---|---|---|
| 一致性强度 | 最终一致,中间状态对外可见 | 更高,Try 阶段不暴露最终状态 |
| 实现复杂度 | 低(只需定义补偿) | 高(需三阶段,处处幂等) |
| 性能 | 高(无锁执行) | 低(Confirm/Cancel 需交互两次) |
| 适用场景 | 长流程、跨多系统(旅行预订) | 短事务、需要严格预留(金融转账) |
| 回滚语义 | 补偿式(逆向操作) | 取消式(丢弃预留) |
| 数据隔离 | 读未提交数据可能被看到 | Try 阶段完全隔离 |
五、生产实战经验总结
5.1 选型指南
- 用 Saga:订单流程、工单审批流、数据同步等长流程场景
- 用 TCC:账户余额扣减、库存强预留、资金划转等短事务强一致场景
- 混合使用:大流程内嵌 TCC 短事务(电商下单:Saga 管流程,TCC 管库存)
5.2 幂等三件套
// 1. 全局唯一事务ID(业务ID+操作类型)
// 2. 状态机严格流转(tried → confirmed/canceled)
// 3. 去重表(bloom filter + DB 唯一键)
const IdempotencyKey = "tx_{bizID}_{step}_{action}"
// Redis + Lua 脚本实现原子幂等检查与记录
const luaScript = `
if redis.call("EXISTS", KEYS[1]) == 1 then
return 0 -- 已处理
end
redis.call("SET", KEYS[1], ARGV[1], "EX", 86400)
return 1 -- 首次处理
`
5.3 超时与重试策略
// 重试配置
retryPolicy := RetryConfig{
MaxAttempts: 3,
InitialDelay: 100 * time.Millisecond,
MaxDelay: 5 * time.Second,
BackoffCoeff: 2.0,
// 超时后执行补偿而非无限重试
OnTimeout: "compensate",
// 异常白名单——哪些异常值得重试
RetryableErrors: []error{
ErrNetworkTimeout,
ErrServiceUnavailable,
},
// 非重试异常,立即触发补偿
NonRetryableErrors: []error{
ErrInsufficientBalance,
ErrAccountFrozen,
},
}
5.4 Saga 可视化与监控
分布式事务失败时,排查链路极其痛苦。生产环境必须有三件套:
- Saga 执行日志:记录每个步骤的起止时间、状态、异常信息
- 分布式追踪:将 saga-id 注入 TraceID,串联全链路
- 自动告警:补偿失败、悬挂事务、超时未完成必须实时告警
// Saga 审计日志
type SagaAuditLog struct {
SagaID string `gorm:"primaryKey"`
BizType string
Status string // executing/completed/compensating/compensated/failed
CurrentStep int
StepDetails JSON // 每个步骤的执行记录
StartedAt time.Time
CompletedAt *time.Time
ErrorMessage string
CompensationAttempts int
}
// 监控告警规则
func sagaHealthCheck() {
// 1. 超过 30 秒的 Saga 仍在执行中 → 告警
// 2. 任何 CompensationFailed → P1 告警(需人工介入)
// 3. 单个集群 Saga 失败率 > 5% → 告警
// 4. 悬挂事务(try confirmed 但无后续)→ 告警
}
5.5 Seata 框架实战经验
Seata 是国内最常用的分布式事务框架,支持 AT、TCC、Saga、XA 四种模式。核心经验:
- AT 模式适合中小企业:自动化程度高、但全局锁在高并发下可能成为瓶颈
- TCC 模式适合核心链路:但必须自己处理幂等、悬挂、空回滚
- Saga 模式适合长流程:Seata 1.5+ 支持状态机引擎,JSON DSL 定义流程
- 存储模式选择:file 仅适合测试,生产用 DB 或 Redis 模式保存事务日志
六、写在最后
分布式事务没有银弹。Saga 牺牲隔离性换取性能与吞吐量,TCC 以复杂度为代价换取更强的数据一致性。在工程实践中,关键不是追求理论完美,而是:
- 充分理解业务容忍度:是否允许中间状态被看到?用户能接受"处理中"吗?
- 幂等贯穿始终:从 Try 到 Cancel,从重试到补偿,每个操作必须有幂等保证
- 自愈能力优先于预防:设计好异常恢复路径,人工兜底 + 自动补偿双管齐下
- 监控驱动治理:事务悬挂、补偿失败必须能在 5 分钟内被发现和定位
只有把"最终一致性"从口号落地为可监控、可恢复、可审计的工程体系,分布式事务才算真正达标。
作者简介:ybb,专注底层系统、内核工程与高性能基础设施架构。

发表评论 取消回复