微服务拆分精讲 / 第 05 章:数据库拆分
第 05 章:数据库拆分
代码拆分是表面,数据库拆分才是灵魂。数据不独立,微服务就是假的。
5.1 为什么数据库拆分最难
5.1.1 代码与数据的拆分难度对比
代码拆分 数据库拆分
───────── ──────────────
• 接口边界清晰 • 关联查询复杂
• 可以渐进式重构 • 数据迁移风险高
• 失败影响可控 • 一致性保证困难
• 自动化测试支持好 • 历史数据量大
• IDE 支持完善 • 需要双写/同步机制
难度:★★☆☆☆ 难度:★★★★★
5.1.2 数据库拆分面临的挑战
| 挑战 | 说明 | 影响 |
|---|
| 跨表 JOIN | 拆分后无法直接 JOIN | 查询方式彻底改变 |
| 分布式事务 | 跨库事务无法用本地事务 | 数据一致性风险 |
| 数据迁移 | 海量数据从旧库迁移到新库 | 停机时间、数据丢失风险 |
| 引用完整性 | 外键约束跨库无法维护 | 需要应用层保证 |
| 全局查询 | 跨库统计报表困难 | 需要额外方案 |
| 数据冗余 | 可能需要冗余部分数据 | 数据同步复杂 |
5.2 数据库拆分策略
5.2.1 三种拆分模式
┌──────────────────────────────────────────────────────────┐
│ 数据库拆分策略 │
├──────────────────────────────────────────────────────────┤
│ │
│ 模式1:共享数据库 (Anti-Pattern) │
│ ┌────────────────────────────────────┐ │
│ │ 共享数据库 │ │
│ │ ┌──────┐ ┌──────┐ ┌──────┐ │ │
│ │ │用户表│ │订单表│ │商品表│ │ │
│ │ └──────┘ └──────┘ └──────┘ │ │
│ └──────────┬─────────────────────────┘ │
│ ┌────┼────┐ │
│ ▼ ▼ ▼ │
│ 用户 订单 商品 │
│ 服务 服务 服务 │
│ │
│ 模式2:Schema 分离 │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ user_db │ │ order_db │ │ product_db│ │
│ │ (同一实例) │ │ (同一实例) │ │ (同一实例) │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ ▲ ▲ ▲ │
│ │ │ │ │
│ 用户服务 订单服务 商品服务 │
│ │
│ 模式3:独立数据库 │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 用户DB │ │ 订单DB │ │ 商品DB │ │
│ │ (独立实例) │ │ (独立实例) │ │ (独立实例) │ │
│ │ MySQL │ │ MySQL │ │ ES+Redis │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ ▲ ▲ ▲ │
│ │ │ │ │
│ 用户服务 订单服务 商品服务 │
└──────────────────────────────────────────────────────────┘
5.2.2 推荐的渐进式拆分路径
共享数据库 (现状)
│
▼ 阶段1:逻辑分离
Schema 分离 (同一实例)
│
▼ 阶段2:接口化
通过 API 访问 (不再跨库查询)
│
▼ 阶段3:物理分离
独立数据库实例
│
▼ 阶段4:按需选型
不同服务使用不同类型的数据库
5.3 分库分表
5.3.1 分库策略
| 策略 | 说明 | 适用场景 |
|---|
| 垂直分库 | 按业务模块拆分数据库 | 微服务化拆分(本章重点) |
| 水平分库 | 同一业务数据按规则分散到多个库 | 单表数据量超大(>5000 万行) |
垂直分库(按业务拆分)
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ 原始DB │ ──▶ │ 用户DB │ │ 订单DB │ │ 商品DB │
│ 用户表 │ │ 用户表 │ │ 订单表 │ │ 商品表 │
│ 订单表 │ │ 地址表 │ │ 订单项表 │ │ 分类表 │
│ 商品表 │ │ 认证表 │ │ 支付表 │ │ 库存表 │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
水平分库(按数据拆分)
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 订单DB │ ──▶ │ 订单DB_0 │ │ 订单DB_1 │
│ (全部订单) │ │ (0-999万) │ │(1000-2000万)│
└──────────┘ └──────────┘ └──────────┘
5.3.2 分表策略
| 策略 | 规则 | 优点 | 缺点 |
|---|
| Range 分表 | 按范围(如时间、ID区间) | 扩展简单 | 可能数据不均 |
| Hash 分表 | 按字段 Hash 取模 | 数据均匀 | 扩容困难 |
| 一致性 Hash | Hash 环 | 扩容友好 | 实现复杂 |
Hash 分表示例(按 user_id % 4)
user_id = 1001 → 1001 % 4 = 1 → order_1
user_id = 1002 → 1002 % 4 = 2 → order_2
user_id = 1003 → 1003 % 4 = 3 → order_3
user_id = 1004 → 1004 % 4 = 0 → order_0
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│order_0 │ │order_1 │ │order_2 │ │order_3 │
│id%4=0 │ │id%4=1 │ │id%4=2 │ │id%4=3 │
└─────────┘ └─────────┘ └─────────┘ └─────────┘
5.3.3 分库分表中间件
| 中间件 | 类型 | 特点 | 适用场景 |
|---|
| ShardingSphere | 代理/嵌入 | Apache 顶级项目,生态完善 | Java 项目首选 |
| MyCat | 代理 | 独立部署,对应用透明 | 传统架构改造 |
| Vitess | 代理 | YouTube 开源,K8s 原生 | 大规模 MySQL |
| CockroachDB | 分布式DB | 自动分片,兼容 PostgreSQL | 新项目首选 |
| TiDB | 分布式DB | 兼容 MySQL,HTAP | 大数据量场景 |
5.4 数据同步方案
5.4.1 同步方式总览
| 方式 | 时效性 | 复杂度 | 适用场景 |
|---|
| CDC(变更数据捕获) | 准实时 | 中 | 数据库拆分后同步 |
| 事件驱动同步 | 准实时 | 中 | 业务事件触发同步 |
| 定时批量同步 | 分钟级 | 低 | 非实时要求的数据 |
| 双写 | 实时 | 高 | 过渡期方案 |
| API 查询 | 实时 | 低 | 查询量少的场景 |
5.4.2 CDC(Change Data Capture)
CDC 通过监听数据库的变更日志(如 MySQL Binlog)来捕获数据变更,是数据库拆分后数据同步的首选方案。
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ 源数据库 │ │ Debezium │ │ Kafka │ │ 目标数据库│
│ MySQL │───▶│ (CDC) │───▶│ (消息) │───▶│ 订单DB │
│ │ │ │ │ │ │ │
│ Binlog │ │ 读取 │ │ 传输 │ │ 消费写入 │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
延迟:< 1 秒
可靠性:高(基于日志,不丢数据)
Debezium 配置示例:
{
"name": "mysql-source-connector",
"config": {
"connector.class": "io.debezium.connector.mysql.MySqlConnector",
"database.hostname": "mysql-source",
"database.port": "3306",
"database.user": "cdc_user",
"database.password": "****",
"database.server.id": "1",
"topic.prefix": "dbserver1",
"database.include.list": "user_db",
"table.include.list": "user_db.users,user_db.addresses"
}
}
5.4.3 事件驱动同步
用户服务 订单服务
┌──────────┐ ┌──────────┐
│ │ UserUpdated │ │
│ 修改用户 │─────────────▶│ 更新本地 │
│ 信息 │ (事件) │ 用户快照 │
└──────────┘ └──────────┘
│ │
▼ ▼
┌──────────┐ ┌──────────┐
│ 用户DB │ │ 订单DB │
│ (权威数据) │ │ (冗余数据) │
└──────────┘ └──────────┘
5.5 数据一致性方案
5.5.1 一致性模型对比
| 模型 | 说明 | 一致性级别 | 性能 | 复杂度 |
|---|
| 强一致性 | 写入后立即可读 | 最高 | 最低 | 最高 |
| 最终一致性 | 写入后一段时间可读 | 较高 | 较高 | 中 |
| 因果一致性 | 保证因果关系的顺序 | 中 | 中 | 中 |
5.5.2 跨服务查询的解决模式
问题:订单服务需要查询用户信息,但用户数据在用户服务的数据库中。
方案1:API 调用(推荐)
────────────────────────
订单服务 ──API──▶ 用户服务 ──▶ 用户DB
优点:数据自治 缺点:网络开销、延迟
方案2:数据冗余
────────────────────────
订单服务的DB中冗余用户基本信息(userId, userName)
通过事件订阅保持同步
优点:查询快 缺点:数据可能不一致
方案3:CQRS + 物化视图
────────────────────────
写操作:各服务写自己的DB
读操作:查询聚合视图(ES/数据仓库)
优点:查询灵活 缺点:架构复杂
5.5.3 数据冗余的同步策略
┌──────────────┐ ┌──────────────┐
│ 用户服务 │ │ 订单服务 │
│ │ │ │
│ 更新用户 │ │ 查询订单 │
│ 发布事件: │ │ (含用户信息) │
│ UserUpdated │ │ │
└──────┬───────┘ └──────┬───────┘
│ │
▼ │
┌─────────┐ │
│ Kafka │ │
│ Topic │ │
└────┬────┘ │
│ │
▼ │
┌──────────────┐ │
│ 订单服务 │ │
│ 消费事件 │ │
│ 更新本地 │ │
│ 用户快照 │──────────┘
└──────────────┘
最终一致性窗口:通常 < 1 秒
5.6 业务场景:电商平台的数据库拆分实战
5.6.1 拆分前状态
单库(MySQL)
├── user 表 (500 万行)
├── user_address 表 (800 万行)
├── product 表 (200 万行)
├── product_sku 表 (1000 万行)
├── order 表 (2 亿行)
├── order_item 表 (5 亿行)
├── payment 表 (1 亿行)
└── inventory 表 (1000 万行)
5.6.2 拆分步骤
| 步骤 | 操作 | 验证 | 回滚方案 |
|---|
| 1 | 创建独立 Schema (user_db, order_db, …) | Schema 间无直接访问 | 删除 Schema |
| 2 | 应用层代码改造,通过 Service 访问 | 所有跨模块查询改用 API | 代码回滚 |
| 3 | 部署双写逻辑(写旧库 + 写新库) | 数据一致性校验 | 停止双写 |
| 4 | 切换读路径到新库 | 查询结果一致性 | 切回旧库 |
| 5 | 停止写旧库 | 无数据丢失 | 恢复双写 |
| 6 | 物理分离数据库实例 | 独立运行稳定 | 迁移回单实例 |
| 7 | 清理旧表中的冗余数据 | 业务正常 | 备份恢复 |
5.6.3 数据迁移方案
全量迁移 + 增量同步
┌───────────────┐ ┌───────────────┐
│ 源数据库 │ │ 目标数据库 │
│ MySQL │ │ MySQL │
└───────┬───────┘ └───────▲───────┘
│ │
│ 1. 全量迁移 (DataX/Dumper) │
└──────────────────────────────┘
│ │
│ 2. 增量同步 (Debezium CDC) │
└──────────────────────────────┘
│ │
│ 3. 数据校验 (checksum) │
└──────────────────────────────┘
│ │
│ 4. 切换读写 │
└──────────────────────────────┘
5.7 CQRS 模式
5.7.1 命令查询职责分离
CQRS(Command Query Responsibility Segregation)将读写操作分离到不同的模型中:
┌────────────────────────────────────────────────────┐
│ CQRS 架构 │
├────────────────────────────────────────────────────┤
│ │
│ 写侧 (Command) 读侧 (Query) │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ 命令处理器 │ │ 查询处理器 │ │
│ │ │ │ │ │
│ │ Command ──▶ │ │ Query ──▶ │ │
│ │ Handler │ │ Handler │ │
│ └──────┬───────┘ └──────▲───────┘ │
│ │ │ │
│ ▼ │ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ 写数据库 │ ──同步──▶│ 读数据库 │ │
│ │ (MySQL) │ │ (ES/Redis) │ │
│ └──────────────┘ └──────────────┘ │
│ │
│ 特点:模型优化写入 特点:模型优化查询 │
│ 强一致性 高性能 │
└────────────────────────────────────────────────────┘
5.7.2 适用场景
| 场景 | 是否适用 CQRS | 说明 |
|---|
| 读写比例严重不均 | ✅ 适用 | 读远多于写,如商品详情页 |
| 复杂查询需求 | ✅ 适用 | 多维度查询,如报表系统 |
| 简单 CRUD | ❌ 不适用 | 增加复杂度无收益 |
| 强一致性要求 | ⚠️ 需评估 | CQRS 天然是最终一致性 |
⚠️ 注意事项
- 先优化查询再拆库——很多性能问题加索引、优化 SQL 就能解决
- 避免分布式 JOIN——如果两个表经常 JOIN,考虑放在同一个服务
- 保留数据校验脚本——迁移后务必验证数据完整性
- 考虑时区和编码——迁移时注意字符集和时区的一致性
- 备份!备份!备份!——数据库操作前必须有完整的备份
📖 扩展阅读
- Martin Fowler - DatabasePerService — 每个服务独立数据库的权威描述
- Debezium Documentation — CDC 方案的首选工具
- Apache ShardingSphere — 分库分表中间件
- Designing Data-Intensive Applications — Martin Kleppmann — 数据密集型系统设计
- Microservices Patterns Chapter 7 — Chris Richardson — 数据拆分模式
本章小结
| 要点 | 说明 |
|---|
| 拆分路径 | 共享库 → Schema 分离 → 接口化 → 独立库 |
| 数据同步 | CDC (Debezium) 是准实时同步的首选方案 |
| 一致性 | 最终一致性是常态,强一致性需要特殊处理 |
| 查询模式 | CQRS + 数据冗余解决跨服务查询问题 |
| 迁移策略 | 全量迁移 + 增量同步 + 数据校验 |
📌 下一章:第 06 章:API 网关 — 统一入口、路由、限流和认证授权。