5.9 KiB
5.9 KiB
tags, create time
| tags | create time | ||||
|---|---|---|---|---|---|
|
2026-05-05 |
数据库拆分
概述
微服务的核心设计原则是 "每个服务拥有独立数据库",这意味着每个服务的表结构、数据存储、甚至数据库类型都可以不同。但当单表数据量持续增长时,就面临拆分的需求。
graph LR
S1[订单服务 DB]
S2[库存服务 DB]
subgraph BAD["反模式:共享数据库"]
S1 --- SHARED[(共享 DB)]
S2 --- SHARED
end
style SHARED fill:#ffebee,stroke:#ef5350
[!failure] 反模式警告 如果两个服务连接同一个数据库的同一张表,它们就不再是独立的微服务——你得到的是分布式单体。服务可以随意互相查询彼此的数据,失去了边界和自治性。
垂直拆分 vs 水平拆分
垂直拆分(按业务域)
按 微服务边界 拆库——这是微服务的标配。
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 |
| 天然解耦 | 服务间不能直接查对方库 |
水平拆分(分库分表)
当单个表的记录量达到千万级以上,需要进一步拆分。
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 配置示例
# 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 做关联查询 | ⭐⭐⭐⭐ | 中高 | 复杂搜索 + 聚合 |
冗余字段实践
-- 订单表冗余关键字段
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 是该时刻的"快照"——它反映的是下单时的状态,不是当前状态。这符合业务语义。
如果需要批量更新冗余字段(如用户头像),通过消息队列异步通知订单服务更新。
数据库迁移工具
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 限界上下文与数据库拆分的对应关系