This commit is contained in:
2026-05-24 11:42:38 +08:00
commit 30d312ac35
521 changed files with 146481 additions and 0 deletions
@@ -0,0 +1,198 @@
---
tags: [microservice, database, sharding, replication]
create time: 2026-05-05
---
# 数据库拆分
## 概述
微服务的核心设计原则是 **"每个服务拥有独立数据库"**,这意味着每个服务的表结构、数据存储、甚至数据库类型都可以不同。但当单表数据量持续增长时,就面临拆分的需求。
```mermaid
graph LR
S1[订单服务 DB]
S2[库存服务 DB]
subgraph BAD["反模式:共享数据库"]
S1 --- SHARED[(共享 DB)]
S2 --- SHARED
end
style SHARED fill:#ffebee,stroke:#ef5350
```
> [!failure] 反模式警告
> 如果两个服务连接同一个数据库的同一张表,它们就不再是独立的微服务——你得到的是**分布式单体**。服务可以随意互相查询彼此的数据,失去了边界和自治性。
## 垂直拆分 vs 水平拆分
### 垂直拆分(按业务域)
按 **微服务边界** 拆库——这是微服务的标配。
```mermaid
graph TB
subgraph "DB-Order"
orders[orders]
order_items[order_items]
order_status[order_status]
end
subgraph "DB-User"
users[users]
user_profiles[user_profiles]
user_addresses[user_addresses]
end
subgraph "DB-Product"
products[products]
categories[categories]
product_images[product_images]
end
```
| 特点 | 说明 |
|------|------|
| 每个服务独占一个数据库 | 物理隔离,互不干扰 |
| 可异构选型 | 订单用 MySQL,用户用 PostgreSQL,搜索用 Elasticsearch |
| 天然解耦 | 服务间不能直接查对方库 |
### 水平拆分(分库分表)
当单个表的记录量达到千万级以上,需要进一步拆分。
```mermaid
graph TB
subgraph "分库策略"
DB1[(DB-01)]
DB2[(DB-02)]
DB3[(DB-03)]
end
subgraph "order_0001 表"
Row1[userId=1 → order_0001]
Row2[userId=4 → order_0001]
end
subgraph "order_0002 表"
Row3[userId=2 → order_0002]
Row4[userId=5 → order_0002]
end
userId_mod["ORDER BY user_id % 2"] --> DB1
userId_mod --> DB2
```
### 常见分片策略
| 策略 | 哈希公式 | 优点 | 缺点 |
|------|---------|------|------|
| **Hash Mod** | `user_id % N` | 简单高效,路由确定 | 扩缩容困难,数据迁移成本高 |
| **Range** | `user_id BETWEEN x AND y` | 范围查询友好 | 热点用户集中到单分片 |
| **Time-based** | `year_month` | 按生命周期管理 | 新分片写入压力大 |
| **Geo-based** | 按地域分片 | 本地化访问,延迟低 | 跨区域操作复杂 |
### ShardingSphere / MyCat
> [!tip] 推荐中间件
>
> **Apache ShardingSphere** 是国内使用最广泛的分库分表方案:
> - 支持 JDBC / Proxy / Sidecar 三种部署模式
> - 内置分片算法:Mod、Range、Hash、Tag
> - 分布式主键生成器(Snowflake)原生集成
> - 读写分离、强制路由、广播表等高级特性
#### ShardingSphere 配置示例
```yaml
# sharding-jdbc 配置
sharding jdbc:
data-sources:
ds0: { type: HikariCP, ... }
ds1: { type: HikariCP, ... }
sharding:
tables:
orders:
actual-data-nodes: ds$->{0..1}.orders$->{0..1}
table-strategy:
standard:
sharding-column: user_id
sharding-algorithm-name: user-id-mod
key-generate-strategy:
column: order_id
key-generator-name: snowflake
sharding-algorithms:
user-id-mod:
type: MOD
props:
sharding-count: 2
```
## 跨库查询方案
> [!question] 经典难题
> 订单服务需要展示用户的姓名和手机号来做收货地址。但用户信息在用户库,订单数据在订单库——怎么办?
### 方案对比
| 方案 | 描述 | 性能 | 复杂度 | 适用场景 |
|------|------|------|--------|---------|
| **冗余字段** | 订单表存用户名字段 | ⭐⭐⭐⭐⭐ | 低 | 只读字段,变更频率低 |
| **接口组装** | 先查订单,再调用户服务补全 | ⭐⭐⭐ | 中 | 偶尔需要关联的场景 |
| **CQRS / 宽表** | 异步同步一份宽表用于查询 | ⭐⭐⭐⭐ | 高 | 高频关联查询 |
| **搜索引擎** | ES/Kibana 做关联查询 | ⭐⭐⭐⭐ | 中高 | 复杂搜索 + 聚合 |
### 冗余字段实践
```sql
-- 订单表冗余关键字段
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL,
username VARCHAR(64), -- 冗余用户名(快照,不参与编辑)
phone VARCHAR(20), -- 冗余手机号(脱敏存储)
created_at TIMESTAMP DEFAULT NOW(),
INDEX idx_user (user_id)
);
```
> [!note] 冗余数据的维护
>
> 用户改名字了怎么办?**不改订单表**。订单上的 username 是该时刻的"快照"——它反映的是下单时的状态,不是当前状态。这符合业务语义。
>
> 如果需要批量更新冗余字段(如用户头像),通过消息队列异步通知订单服务更新。
## 数据库迁移工具
```mermaid
graph LR
Dev["开发环境"] -->|"flyway migrate"| Stage["Staging"]
Stage -->|"人工审批"| Prod["Production"]
Prod -->|"flyway validate"| Check["校验版本一致性"]
```
| 工具 | 语言 | 特点 |
|------|------|------|
| **Flyway** | Java | 基于文件命名,SQL 脚本方式,简单直观 |
| **Liquibase** | Java | XML/YAML/JSON 格式,支持回滚生成 |
| **golang-migrate** | Go | CLI 工具,轻量,适合 Go 项目 |
### Flyway 迁移流程
```
db/migration/
├── V1__create_users_table.sql
├── V2__add_user_phone.sql
├── V3__create_orders_table.sql
└── V4__add_order_status_enum.sql
```
执行顺序:V1 → V2 → V3 → V4。Flyway 内部维护 `_schema_version` 表追踪已执行的迁移。
## 关联笔记
- [[03-数据一致性/02-分布式事务]] — 拆分后的数据一致性问题
- [[01-基础概念]] — DDD 限界上下文与数据库拆分的对应关系
@@ -0,0 +1,249 @@
---
tags: [microservice, distributed-transactions, saga, tcc, outbox]
create time: 2026-05-05
---
# 分布式事务
## 概述
每个服务拥有独立数据库,跨服务的"一次操作"实际上涉及**多个本地事务**。如何保证这些本地事务要么全部成功、要么全部回滚,就是分布式事务要解决的问题。
```mermaid
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 模式(推荐)
```mermaid
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 表建表示例
```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';
```
```go
// 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 模式直接用事务消息:
```go
// 发送事务消息
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` | 金额调整类操作 |
```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 !isUniqueViolation(err) {
log.Error("process message failed", err)
return
}
// 执行业务逻辑——到这里说明消息是新到达的
```
## 方案二:Saga 模式
Saga 适用于**跨多个服务的长流程操作**,将大事务拆成一系列本地小事务,每个步骤都有对应的补偿操作。
### 编排式 vs 编舞式
```mermaid
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 时序图
```mermaid
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) | 低(只需正向 + 反向操作) |
| 性能 | 中(需要两阶段提交) | 中高(单阶段本地事务) |
| 适用场景 | 资金、库存等高敏感业务 | 订单流程、审批流等业务链 |
## 关联笔记
- [[03-数据一致性/01-数据库拆分]] — 数据库拆分是分布式事务的前提
- [[02-服务治理/06-容错模式]] — 熔断器和重试在分布式事务中的作用
- [[02-服务治理/05-服务间通信]] — 消息投递的一致性保障
+157
View File
@@ -0,0 +1,157 @@
---
tags: [microservice, id-generation, snowflake, uuid, distributed-id]
create time: 2026-05-05
---
# 分布式 ID 生成
## 概述
在微服务架构中,数据库被拆分成多个独立实例,不再共享自增主键。如何生成分布式环境下全局唯一的 ID,是一个经典问题。
```mermaid
graph LR
A["❌ 数据库 AUTO_INCREMENT"] -->|"分库后冲突"| Problem["不可用"]
B["✅ 分布式 ID"] --> Unique["全局唯一"]
B --> Ordered["趋势有序"]
B --> HighThroughput["高吞吐"]
style Problem fill:#ffebee
style Unique fill:#e8f5e9
style Ordered fill:#e8f5e9
style HighThroughput fill:#e8f5e9
```
> [!question] 为什么要自己生成?不用数据库自增?
>
> - **分库后**:每个库的自增 ID 从 1 开始,必然冲突
> - **性能瓶颈**:DB 序列或号段模式在高并发下成为瓶颈
> - **耦合度高**:ID 生成依赖特定数据库类型
## 主流方案对比
| 方案 | 唯一性保证 | 有序性 | 性能 (QPS) | 复杂度 | 适用场景 |
|------|-----------|--------|-----------|--------|---------|
| **UUID** | MD5/SHA 哈希保证 | ❌ 完全无序 | ⭐⭐⭐⭐⭐ | 极低 | 临时标识、缓存 Key |
| **雪花算法 (Snowflake)** | WorkerId + 时间戳 | ✅ 趋势有序 | ⭐⭐⭐⭐⭐ | 中 | 通用方案,首选 |
| **号段模式** | DB 批量发放 | ✅ 严格有序 | ⭐⭐⭐⭐ | 中 | 已有 DB 架构改造 |
| **Leaf (美团)** | Snowflake + 号段 | ✅ 趋势/严格有序 | ⭐⭐⭐⭐⭐ | 中高 | 大规模生产环境 |
| **NanoID** | Base62 + CSPRNG | ❌ 无序 | ⭐⭐⭐⭐⭐ | 低 | API Token、短链 |
## 雪花算法 (Snowflake)
Twitter 开源的经典算法,将 ID 分为三部分:
```
64 bit: 1 bit(符号位) + 41 bit(时间戳) + 10 bit(机器ID) + 12 bit(序列号)
│ │ │ │
│ │ │ └─ 每毫秒最多 4096 个 ID
│ │ └──────────────── 1024 台机器
│ └────────────────────────────── ~69 年跨度
└────────────────────────────────────────── 固定为 0
```
### 核心公式
```go
// 伪代码
func generateID() int64 {
timestamp = currentMillis() // 当前毫秒时间戳(相对时间戳)
workerId = 1 // 机器编号 (0-1023)
sequence = incrementSequence() // 递增序号 (0-4095)
return (timestamp << 22) | // 时间戳占高位
(workerId << 12) | // 机器 ID 放中间
sequence // 序列号放在低位
}
```
### Go 实现要点
```go
type Snowflake struct {
mu sync.Mutex
lastTime int64
sequence int64
workerId int64
workerBits uint8 = 10
stepBits uint8 = 12
}
func (sf *Snowflake) NextID() int64 {
sf.mu.Lock()
defer sf.mu.Unlock()
now := time.Now().UnixMilli()
if now < sf.lastTime {
panic("clock moved backwards") // 时钟回拨保护
}
if now == sf.lastTime {
sf.sequence++
} else {
sf.sequence = 0
}
sf.lastTime = now
return (now << 22) | (sf.workerId << 12) | sf.sequence
}
```
### 注意事项
| 问题 | 解决方案 |
|------|---------|
| **时钟回拨** | 等待到回拨前的时间点再继续;或分配备用 WorkerId |
| **WorkerId 分配** | 启动时从注册中心获取,或使用 K8s Pod Name 映射 |
| **单机 QPS 上限** | 单 WorkerId × 单机房约 409.6 万/秒,通常够用 |
| **跨代际兼容性** | 新字段插入会改变位移量,设计时需预留比特位 |
## UUID v7 (时间有序 UUID)
如果你不想维护 Snowflake 的 WorkerId,可以考虑 **UUID v7**——它是 RFC 9562 定义的新版本,保持了时序性。
```
UUID v7: 48-bit timestamp + 12-bit rand_a + 4-bit version + 12-bit rand_b + 2-bit variant + 62-bit rand_c
```
| 维度 | UUID v4 | UUID v7 |
|------|---------|---------|
| **生成方式** | 纯随机 | 时间戳 + 随机数 |
| **有序性** | ❌ 完全随机 | ✅ 趋势有序 |
| **索引效率** | ❌ 随机插入导致页分裂 | ✅ 顺序插入友好 |
| **存储大小** | 16 bytes | 16 bytes |
| **实现复杂度** | 极低 | 低(标准库支持) |
> [!tip] 如果你的数据库是 MySQL 5.7+,直接改用 `BINARY(16)` 存储 UUID v7,比 BIGINT AUTO_INCREMENT 更优。
## 号段模式
基于数据库分批领取 ID 区间:
```mermaid
sequenceDiagram
App->>DB: SELECT max_id FROM id_generator WHERE biz='order'
DB-->>App: max_id = 10000, step = 2000
Note over App: 内存中维护 [10000, 12000) 号段
loop 每次请求
App->>App: ID++ (本地原子操作)
end
App->>DB: UPDATE id_generator SET max_id = 12000 WHERE max_id = 10000
DB-->>App: ACK
Note over App: 下次取号段: [12000, 14000)
```
**优点**:兼容现有 MySQL 架构,无需引入外部组件。
**缺点**:存在 ID 浪费(宕机未用完的号段),极端情况下可能产生空洞。
## 关联笔记
- [[03-数据一致性/01-数据库拆分]] — 分库分表场景下的 ID 生成
- [[03-数据一致性/02-分布式事务]] — Outbox 消息 ID 也需要全局唯一
+39
View File
@@ -0,0 +1,39 @@
---
tags: [microservice, data-consistency]
create time: 2026-04-29 12:03
---
# 数据一致性
## 概述
微服务的核心设计原则是 **"每个服务拥有独立数据库"**,这带来了分布式事务和数据一致性的经典难题。本文梳理主要解决方案及其取舍。
## 知识体系
```mermaid
graph LR
A["数据库拆分"] --> B["分布式事务"]
B --> C["ID 生成"]
style A fill:#e3f2fd
style B fill:#fff3e0
style C fill:#e8f5e9
```
| # | 主题 | 核心问题 |
|---|------|----------|
| 1 | [[03-数据一致性/01-数据库拆分]] | 每个服务独立 DB,表大了怎么拆分?跨库查询怎么做? |
| 2 | [[03-数据一致性/02-分布式事务]] | 本地事务 + MQ、Saga、TCC,哪个方案最合适? |
| 3 | [[03-数据一致性/03-ID生成]] | 没有自增主键了,全局唯一 ID 怎么生成? |
### 学习建议
> [!tip] 学习路径
> 先理解"为什么不能共享数据库",再学分库分表的策略,最后深入分布式事务方案选型。ID 生成是最轻量的话题,可以随时了解。
### 关联笔记
- [[01-基础概念]] — 微服务拆分与独立数据库原则
- [[02-服务治理]] — 服务调用链路上的超时、重试、熔断
- [[hzh/MS/README.md]] — 完整微服务知识索引