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/hzh/MS/03-数据一致性.md
T

7.7 KiB
Raw Blame History

tags, create time
tags create time
microservice
distributed-system
database
data-consistency
saga-pattern
tcc
idempotency
outbox-pattern
2026-04-29 12:03

数据一致性

概述

微服务的核心设计原则是 "每个服务拥有独立数据库",这带来了分布式事务和数据一致性的经典难题。本文梳理主要解决方案及其取舍。

为什么不能共享数据库

graph LR
    S1[订单服务 DB]
    S2[库存服务 DB]
    
    subgraph BAD["反模式:共享数据库"]
        S1 --- SHARED[(共享 DB)]
        S2 --- SHARED
    end

[!failure] 反模式警告 如果两个服务连接同一个数据库的同一张表,它们就不再是独立的微服务——你得到的是分布式单体。服务可以随意互相查询彼此的数据,失去了边界和自治性。

分布式事务方案概览

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

[!info] 选择策略 先默认用 本地事务 + 异步事件(最简单、最高效),只有在业务明确需要 Saga 或 TCC 时才升级。

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

这是最常用的方案,核心思想是将"数据变更 + 发消息"合并到一个本地事务中。

sequenceDiagram
    participant U as 用户
    participant O as 订单服务
    participant MQ as 消息队列
    participant I as 库存服务

    U->>O: 创建订单
    O->>O: BEGIN 事务
    O->>O: 插入订单记录
    O->>MQ: 发送订单创建消息
    O->>O: COMMIT 事务
    MQ-->>I: 投递消息
    I->>I: 扣减库存
    I-->>O: 返回结果(可选)

实战:电商订单创建流程

以「用户下单」为例,这个流程涉及 订单服务(创建订单)和 库存服务(扣减库存),是典型的跨服务数据一致性问题:

步骤 动作 一致性保障点
1 用户点击"提交订单" 前端防重复提交(按钮置灰 / Token 机制)
2 订单服务写入订单表 + 发送 order.created 消息 事务边界:两件事必须在同一个本地事务中完成
3 消息队列投递给库存服务 保证消息至少一次投递(At-Least-Once)
4 库存服务扣减库存 幂等性:防止消息重投导致库存被扣两次
5 库存不足时回滚订单 通过 Sagas 补偿:已创建的订单标记为"取消"

[!question] 思考 如果步骤 2 的数据库写成功了,但 MQ 发消息失败了,会发生什么?用户看到下单成功但实际上库存没被锁定。这说明为什么必须用事务消息或本地消息表,而不是直接调 MQ API。


如何保证"事务内既写库又发消息"?

方案 A:事务消息(推荐)

  • RocketMQ /阿里云 MSE 原生支持事务消息
  • MQ 回查机制确保消息不丢

方案 B:本地消息表

  • 在业务库里建一张 outbox 表
  • 业务事务同时写入业务数据和消息记录
  • 后台定时任务扫描未发送的消息推送出去
// 本地消息表方案示意
tx, _ := db.Begin()
// 1. 业务操作
tx.Exec("UPDATE accounts SET balance = balance - ? WHERE id = ?", amount, fromID)
// 2. 记录待发送消息(带状态字段)
tx.Exec("INSERT INTO outbox (topic, payload, status, created_at) VALUES (?, ?, 'pending', NOW())", "order.created", jsonPayload)
tx.Commit()
// 后台进程轮询 outbox 表中 status='pending' 的记录,发送到 MQ 后更新为 'sent'

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';

消费幂等性

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

三种常见策略:

策略 实现方式 适用场景
数据库唯一约束 用 msg_id 或业务主键做 UNIQUE 最可靠,推荐首选
Redis 去重键 SET 一个 TTL Key(如 dedup:{msg_id} 过期 24h) 高吞吐场景,需考虑 Redis 可用性
乐观锁版本控制 UPDATE 时加 WHERE version = old_version 适用于金额调整类操作
// 推荐方案:利用 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 err != nil {
    // 其他错误才需要处理,UNIQUE 冲突直接忽略
    if !isUniqueViolation(err) {
        log.Error("process message failed", err)
    }
}
// 执行业务逻辑...

[!tip] 设计要点 ON CONFLICT DO NOTHING 比先查后插更可靠——它消除了竞态窗口。即使用两个实例同时收到同一条消息,只会有一个成功写入。

[!question] 思考 为什么要在生产者侧保证消息可靠投递,而不是靠消费者反复拉取重试?

方案二:Saga 模式

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

graph LR
    S1[① 创建订单<br/>→ 取消订单]
    S2[② 扣减库存<br/>→ 恢复库存]
    S3[③ 支付扣款<br/>→ 退款]
    S4[④ 发货<br/>→ 无补偿]
    
    S1 --> S2 --> S3 --> S4

编排式 vs 编舞式:

graph TB
    subgraph ORCHESTRATION["编排式 — 中心协调器"]
        CO[Coordinator] --> S1[OrderSvc]
        CO --> S2[InventorySvc]
        CO --> S3[PaymentSvc]
    end

    subgraph CHOREOGRAPHY["编舞式 — 事件驱动"]
        E1[订单创建] --> S11[订单服务]
        S11 --> E2[库存不足事件]
        E2 --> S12[库存服务]
        S12 --> E3[扣减完成事件]
        E3 --> S13[支付服务]
    end
编排式 编舞式
控制流 中心化 Coordinator 各服务通过事件自发响应
可观测性 ✅ 集中管理 ❌ 流程散布在各服务
耦合度 依赖 Coordinator 服务间仅感知事件

方案三:TCC 与 AT 模式

[!abstract] 进阶了解

TCC (Try-Confirm-Cancel):每个事务步骤实现三个接口。适合对一致性要求严格且能承受开发成本的业务。

AT 模式 (Seata):框架层自动处理两阶段提交,对业务透明。本质上是基于 undo_log 的增强型 XA,性能低于 Saga。

总结选型指南

[!summary] 决策树

graph TD
    A["跨几个服务"] -->|"1-2"| B["本地事务 + 事件通知"]
    A -->|"3+"| C["Saga 模式"]
    B -->|"强"| D["加幂等,必要时上 TCC"]
    B -->|"弱"| E["本地事务已够"]
    C -->|"不能"| F["考虑 AT 模式"]
    C -->|"能"| G["自行实现补偿"]

经验法则:80% 的场景,本地事务 + MQ 就足够了。先别急着上复杂方案。

关联笔记