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

218 lines
7.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [microservice, distributed-system, database, data-consistency, saga-pattern, tcc, idempotency, outbox-pattern]
create time: 2026-04-29 12:03
---
# 数据一致性
## 概述
微服务的核心设计原则是 **"每个服务拥有独立数据库"**,这带来了分布式事务和数据一致性的经典难题。本文梳理主要解决方案及其取舍。
## 为什么不能共享数据库
```mermaid
graph LR
S1[订单服务 DB]
S2[库存服务 DB]
subgraph BAD["反模式:共享数据库"]
S1 --- SHARED[(共享 DB)]
S2 --- SHARED
end
```
> [!failure] 反模式警告
> 如果两个服务连接同一个数据库的同一张表,它们就不再是独立的微服务——你得到的是**分布式单体**。服务可以随意互相查询彼此的数据,失去了边界和自治性。
## 分布式事务方案概览
| 方案 | 一致性级别 | 性能 | 复杂度 | 适用场景 |
|------|-----------|------|--------|---------|
| 本地事务 + 事件通知 | 最终一致 | 高 | 低 | 绝大多数场景 |
| Saga 模式 | 最终一致 | 中 | 中 | 长流程业务 |
| TCC | 强最终一致 | 中 | 高 | 对一致性要求较高的场景 |
| AT 模式 (Seata) | 伪强一致 | 中低 | 低 | 不想改业务代码时 |
> [!info] 选择策略
> 先默认用 **本地事务 + 异步事件**(最简单、最高效),只有在业务明确需要 Saga 或 TCC 时才升级。
## 方案一:本地事务 + 消息队列
这是**最常用**的方案,核心思想是将"数据变更 + 发消息"合并到一个本地事务中。
```mermaid
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` 表
- 业务事务同时写入业务数据和消息记录
- 后台定时任务扫描未发送的消息推送出去
```go
// 本地消息表方案示意
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 表建表示例
```sql
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` | 适用于金额调整类操作 |
```go
// 推荐方案:利用 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 适用于**跨多个服务的长流程操作**,将大事务拆成一系列本地小事务,每个步骤都有对应的补偿操作。
```mermaid
graph LR
S1[① 创建订单<br/>→ 取消订单]
S2[② 扣减库存<br/>→ 恢复库存]
S3[③ 支付扣款<br/>→ 退款]
S4[④ 发货<br/>→ 无补偿]
S1 --> S2 --> S3 --> S4
```
**编排式 vs 编舞式**:
```mermaid
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] 决策树
>
> ```mermaid
> graph TD
> A["跨几个服务"] -->|"1-2"| B["本地事务 + 事件通知"]
> A -->|"3+"| C["Saga 模式"]
> B -->|"强"| D["加幂等,必要时上 TCC"]
> B -->|"弱"| E["本地事务已够"]
> C -->|"不能"| F["考虑 AT 模式"]
> C -->|"能"| G["自行实现补偿"]
> ```
>
> **经验法则**:80% 的场景,本地事务 + MQ 就足够了。先别急着上复杂方案。
## 关联笔记
- [[01-基础概念]] — 微服务拆分与独立数据库原则
- [[02-服务治理]] — 服务调用链路上的超时、重试、熔断