This repository has been archived on 2026-05-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
all-in-kingsoft/hhs/MS/03-数据一致性/02-分布式事务.md
T
2026-05-17 22:00:24 +08:00

7.7 KiB
Raw Blame History

tags, create time
tags create time
microservice
distributed-transactions
saga
tcc
outbox
2026-05-05

分布式事务

概述

每个服务拥有独立数据库,跨服务的"一次操作"实际上涉及多个本地事务。如何保证这些本地事务要么全部成功、要么全部回滚,就是分布式事务要解决的问题。

graph LR
    A["用户下单"] --> B["订单服务<br/>写库"]
    B --> C["库存服务<br/>扣减"]
    C --> D["支付服务<br/>扣款"]
    
    style A fill:#e3f2fd
    style B fill:#fff3e0
    style C fill:#fff3e0
    style D fill:#fff3e0

分布式事务方案全景

方案 一致性级别 性能 复杂度 适用场景
本地事务 + MQ 事件 最终一致 ⭐⭐⭐⭐⭐ ⭐ 绝大多数场景
Saga 模式 最终一致 ⭐⭐⭐ ⭐⭐ 长流程业务
TCC 强最终一致 ⭐⭐⭐ ⭐⭐⭐⭐ 对一致性要求较高的场景
AT 模式 (Seata) 伪强一致 ⭐⭐ ⭐ 不想改业务代码时

[!tip] 选择策略 先默认用本地事务 + 异步事件(最简单、最高效),只有在业务明确需要 Saga 或 TCC 时才升级。80% 的场景,本地事务 + MQ 就足够了。

方案一:本地事务 + 消息队列

核心思想:将"数据变更 + 发消息"合并到一个本地事务中。

Outbox 模式(推荐)

sequenceDiagram
    participant App as 应用服务
    participant DB as 数据库
    participant Outbox as Outbox 表
    participant MQ as 消息队列
    participant Sub as 订阅方

    App->>DB: BEGIN 事务
    App->>DB: 写业务数据
    App->>Outbox: 写入待发送消息
    App->>DB: COMMIT
    
    loop 定时任务
        MQ->>Outbox: 扫描 status='pending'
        Outbox-->>MQ: 返回消息列表
        MQ->>Sub: 投递消息
        MQ->>Outbox: 更新为 'sent'
    end
    
    Note right of Sub: 至少一次投递 + 消费者幂等

Outbox 表建表示例

CREATE TABLE outbox (
    id          BIGSERIAL PRIMARY KEY,
    topic       VARCHAR(255) NOT NULL,     -- 消息主题
    payload     JSONB      NOT NULL,       -- 消息体
    status      VARCHAR(20)  NOT NULL DEFAULT 'pending', -- pending / sent / failed
    error_msg   TEXT,                       -- 失败原因
    created_at  TIMESTAMP    NOT NULL DEFAULT NOW(),
    sent_at     TIMESTAMP
);

-- 加速定时扫描查询
CREATE INDEX idx_outbox_pending ON outbox (status, created_at) 
WHERE status = 'pending';
// Go 示例:出事务内同时写业务数据和 outbox 记录
tx, _ := db.Begin()
tx.Exec("INSERT INTO orders (user_id, total) VALUES ($1, $2)", userID, total)
tx.Exec(`INSERT INTO outbox (topic, payload, status) 
         VALUES ('order.created', $1, 'pending')`, jsonPayload)
tx.Commit()

// 后台 goroutine 轮询并推送
func outboxWorker(ctx context.Context, ticker *time.Ticker) {
    for {
        select {
        case <-ctx.Done():
            return
        case <-ticker.C:
            sendPendingMessages(ctx)
        }
    }
}

事务消息(RocketMQ 原生支持)

如果使用的是 RocketMQ,可以绕过 Outbox 模式直接用事务消息:

// 发送事务消息
txMsg := rocketmq.NewTransactionMessage("order-created", payload)
localTx := &MyLocalTxChecker{}

// half 消息发送 → 本地事务执行 → 提交/回查
res, _ := producer.SendMessageInTransaction(txMsg, localTx)

RocketMQ 的事务消息流程:

  1. 生产者发送 "half 消息" 到 MQ(消费者不可见)
  2. 执行本地事务
  3. 根据结果 Commit(消费者可见)或 Rollback(丢弃)
  4. 如果步骤 2 超时,MQ 回查本地事务状态

消费幂等性

[!warning] 关键保障 消息可能重复投递(网络超时、MQ 重投),消费者必须做到幂等——处理一次和处理多次的结果完全相同。

三种常见策略:

策略 实现方式 适用场景
数据库唯一约束 msg_id 做 UNIQUE 最可靠,推荐首选
Redis 去重键 SET dedup:{msg_id} 1 NX EX 86400 高吞吐场景
乐观锁版本控制 UPDATE SET qty = qty - N WHERE version = V 金额调整类操作
// 推荐方案:利用 UNIQUE 约束做幂等保障
_, err := db.Exec(`
    INSERT INTO order_events (msg_id, order_id, action, amount)
    VALUES ($1, $2, $3, $4)
    ON CONFLICT (msg_id) DO NOTHING
`, msgID, orderID, action, amount)

if !isUniqueViolation(err) {
    log.Error("process message failed", err)
    return
}
// 执行业务逻辑——到这里说明消息是新到达的

方案二:Saga 模式

Saga 适用于跨多个服务的长流程操作,将大事务拆成一系列本地小事务,每个步骤都有对应的补偿操作。

编排式 vs 编舞式

graph TB
    subgraph ORCHESTRATION["编排式 — Coordinator 中心化"]
        CO[Coordinator] --> S1[OrderSvc: Create]
        CO --> S2[InventorySvc: Reserve]
        CO --> S3[PaymentSvc: Charge]
        
        S1 -.->|失败→Cancel| CO
        S2 -.->|失败→Cancel| CO
        S3 -.->|失败→Cancel| CO
    end

    subgraph CHOREOGRAPHY["编舞式 — 事件驱动"]
        E1[OrderCreated] --> S11[OrderService]
        S11 --> E2[StockReserved]
        E2 --> S12[InventoryService]
        S12 --> E3[PaymentCharged]
        E3 --> S13[PaymentService]
        
        S13 -.-> E4[PaidFailed] -.-> S11
    end
维度 编排式 编舞式
控制流 中心化 Coordinator 各服务通过事件自发响应
可观测性 ✅ 集中管理全流程 ❌ 流程散布在各服务
耦合度 依赖 Coordinator 服务间仅感知事件
适合规模 5~15 步的 Saga 简单链路 (< 5 步)

Saga 补偿设计原则

每个正向操作必须有对应的反向补偿:

正向操作 补偿操作
创建订单 取消订单
预留库存 释放库存
扣款 退款
发送通知 撤销通知(一般不需要)

[!warning] 补偿操作的幂等性 补偿操作也必须幂等——CancelOrder 可能被触发多次。用订单状态的流转来保证(如只有 PENDING 才能转到 CANCELLED)。

方案三:TCC (Try-Confirm-Cancel)

TCC 在每个事务步骤中实现三个接口:

Try:   预留资源(冻结余额 / 锁定库存)
Confirm: 确认使用资源(正式扣减 / 正式锁定)
Cancel:  释放资源(解冻余额 / 解锁库存)

TCC 时序图

sequenceDiagram
    participant Orch as Coordinator
    participant O as Order Service
    participant I as Inventory Service
    participant P as Payment Service
    
    Orch->>O: Try(CreateOrder)
    O-->>Orch: OK
    
    Orch->>I: Try(ReserveStock)
    I-->>Orch: OK
    
    Orch->>P: Try(ChargeBalance)
    P-->>Orch: OK
    
    Orch->>O: Confirm
    Orch->>I: Confirm
    Orch->>P: Confirm
    
    Note over Orch,P: 全部 Confirm → 事务完成

TCC vs Saga 对比

维度 TCC Saga
一致性强度 较强(资源被占用期间不允许其他事务使用) 较弱(中间态数据可见)
开发成本 高(每个业务方法实现 Try/Confirm/Cancel) 低(只需正向 + 反向操作)
性能 中(需要两阶段提交) 中高(单阶段本地事务)
适用场景 资金、库存等高敏感业务 订单流程、审批流等业务链

关联笔记