199 lines
5.9 KiB
Markdown
199 lines
5.9 KiB
Markdown
|
|
---
|
||
|
|
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 限界上下文与数据库拆分的对应关系
|