--- 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[① 创建订单
→ 取消订单] S2[② 扣减库存
→ 恢复库存] S3[③ 支付扣款
→ 退款] S4[④ 发货
→ 无补偿] 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-服务治理]] — 服务调用链路上的超时、重试、熔断