这是《MySQL 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 当单实例容量、写入吞吐、连接数或维护窗口无法满足业务时,需要拆分数据。分库分表是最后的水位手段,必须先穷尽索引、SQL、架构和读写分离优化。
28.1 什么时候需要分
判断信号:
| 维度 | 信号 |
|---|---|
| 容量 | 单表或单实例磁盘持续增长 |
| 写入 | 主库 CPU、IO、Redo、Binlog 达到上限 |
| DDL | 索引维护和大表变更窗口不足 |
| 备份 | 全量备份和恢复时间超过目标 |
| 复制 | 大事务造成长期延迟 |
| 热点 | 单表或单行写入不可拆 |
| 运维 | 单表行数影响统计和归档 |
不建议只按“单表超过 2000 万行”这类绝对值决策。行宽、索引数、访问模式、Buffer Pool 和磁盘能力都影响实际边界。
28.2 先做垂直拆分
垂直拆分按业务或字段拆:
订单库:orders、order_items
用户库:users、profiles
商品库:products、skus
营销库:coupons、activities
字段垂直拆分:
orders:高频核心字段
order_details:低频详情、大字段
优点:
- 边界清晰;
- 故障域隔离;
- 权限和安全更容易;
- 大字段不拖慢核心表;
- 适合微服务治理。
局限:
- 不能解决单业务无限增长;
- 跨库 JOIN 变复杂;
- 需要服务边界治理;
- 分布式事务问题出现。
28.3 水平分片
水平分片把同一张表按规则拆到多个库或表:
orders_00 -> db_00
orders_01 -> db_01
orders_02 -> db_02
orders_03 -> db_03
常见分片方式:
| 方式 | 规则 | 特点 |
|---|---|---|
| Hash | user_id % N |
分布均匀,扩容迁移多 |
| Range | 按时间或 ID 区间 | 便于归档,易热点 |
| 一致性 Hash | 环形映射 | 减少迁移 |
| 基因法 | ID 中嵌入分片基因 | 避免二次查询 |
| 查表 | 保存分片映射表 | 灵活,映射表自身要高可用 |
| 地理位置 | 按城市 / 区域 | 本地化,跨区查询复杂 |
28.4 分片键选择
分片键应满足:
- 大多数核心查询都带它;
- 写入分布均匀;
- 业务生命周期内稳定;
- 不容易变更;
- 能表达数据归属;
- 与交易边界一致。
常见选择:
| 业务 | 通常分片键 |
|---|---|
| 电商订单 | user_id 或 seller_id |
| 支付流水 | payment_no / merchant_id |
| 聊天消息 | conversation_id |
| IoT 时序 | device_id + time |
| SaaS 多租户 | tenant_id |
| 日志检索 | time + source |
矛盾场景:订单既要按买家查,又要按卖家查。常见方案:
- 以买家为主分片;
- 卖家订单异步同步到卖家维度表;
- 后台运营查询走分析库;
- 全局索引服务;
- 双写投影,接受同步延迟。
不要把两个维度都作为强一致实时查询路径。
28.5 全局 ID
分片后自增 ID 不能直接保证全局唯一。
常见方案:
| 方案 | 说明 | 风险 |
|---|---|---|
| 号段模式 | 数据库批量发号 | 中心依赖 |
| 雪花 ID | 时间 + 机器 + 序列 | 时钟回拨 |
| UUID | 全局唯一 | 空间大、无序 |
| Redis INCR | 高性能 | 持久化和中心依赖 |
| 分片前缀 | ID 带分片信息 | 泄露拓扑 |
推荐:
内部主键:趋势递增 BIGINT
业务单号:带日期、类型、机器位或随机位
对外 ID:随机 public_id
雪花 ID 必须处理时钟回拨、机器分配、监控和告警。
28.6 跨片查询
按非分片键查询:
SELECT *
FROM orders
WHERE order_no = '20260825000001';
如果分片键是 user_id,系统需要先知道 order_no 在哪个分片。
方案:
- 订单号嵌入用户 ID 基因;
- 保存全局索引表;
- 使用搜索引擎;
- 异步写投影表;
- 广播所有分片;
- 限制后台广播查询。
广播查询代价:
1 条查询 -> N 个分片 -> N 份执行计划 -> N 份网络往返
必须设置超时、并发限制和结果归并上限。
28.7 跨片事务
尽量避免跨片事务。设计原则:
- 一个交易边界内只操作一个分片;
- 其他动作使用本地状态表;
- 事务消息驱动最终一致;
- 幂等消费;
- 定时对账;
- 人工补偿入口。
本地消息表示例:
CREATE TABLE local_message_outbox (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
message_id VARCHAR(64) NOT NULL,
aggregate_type VARCHAR(32) NOT NULL,
aggregate_id VARCHAR(64) NOT NULL,
payload JSON NOT NULL,
status VARCHAR(16) NOT NULL,
retry_count INT UNSIGNED NOT NULL DEFAULT 0,
next_retry_at DATETIME(3) NOT NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_message_id (message_id),
KEY idx_status_next_retry (status, next_retry_at)
) ENGINE=InnoDB;
业务写入和 outbox 同库同事务,发送器读取 outbox 投递消息。
28.8 中间件路由
常见方式:
| 方式 | 代表 | 特点 |
|---|---|---|
| 客户端 SDK | ShardingSphere-JDBC | 少一层代理,语言绑定 |
| 代理层 | ShardingSphere-Proxy、MyCat | 语言无关,链路长 |
| 服务层路由 | 微服务自行路由 | 灵活,业务侵入 |
| 云原生分发 | 云厂商分布式数据库 | 托管,锁定能力不同 |
中间件要评估:
- SQL 兼容度;
- 事务语义;
- 读写分离;
- 连接数;
- 元数据管理;
- 监控追踪;
- DDL 支持;
- 社区和版本。
28.9 迁移总体流程
1. 明确目标:容量、性能、成本、安全、运维;
2. 设计目标结构和路由;
3. 评估 SQL 和事务改造成本;
4. 搭建目标集群;
5. 全量迁移;
6. 增量同步;
7. 数据校验;
8. 灰度读;
9. 双写或写切换;
10. 观察和回滚窗口;
11. 下线旧链路;
每个阶段都要有 checkpoints、指标和回滚方案。
28.10 全量与增量迁移
全量
按分片键或主键分片读取:
SELECT *
FROM orders
WHERE id > :last_id
ORDER BY id
LIMIT 1000;
要求:
- 任务可断点续跑;
- 每批记录水位;
- 目标端幂等插入;
- 限速;
- 失败可重试。
增量
使用 Binlog CDC:
旧库 Binlog -> 解析 -> 按新分片键写入目标库
需要处理:
- DDL;
- INSERT / UPDATE / DELETE;
- 事务边界;
- 乱序;
- 重复;
- 位点持久化;
- 延迟;
- 大事务;
- 主从切换。
28.11 双写切流
双写流程:
阶段一:写旧库,CDC 同步新库;
阶段二:写旧库为主,同时写新库,新库失败记录;
阶段三:校验一致,读灰度到新库;
阶段四:写新库为主,反向同步旧库;
阶段五:确认稳定,下线旧库;
关键点:
- 以旧库为准或以新库为准必须唯一;
- 双写失败要有补偿队列;
- 不能让两个都成功的路径分叉;
- 写请求带版本或更新时间;
- 删除和状态回退要能同步;
- 保留反向回滚链路。
28.12 数据校验
校验维度:
| 类型 | 示例 |
|---|---|
| 结构 | 表、列、索引、字符集 |
| 数量 | 总行数、分片行数 |
| 聚合 | SUM 金额、COUNT 状态 |
| 抽样 | 主键详情对比 |
| 业务 | 订单和明细关联 |
| 时效 | 增量延迟 |
| 约束 | 唯一键冲突 |
| 性能 | 核心接口耗时 |
示例:
SELECT
COUNT(*) AS row_count,
SUM(total_amount) AS amount_sum,
MAX(updated_at) AS max_updated
FROM orders
WHERE created_at >= '2026-08-01';
两边同口径执行,不能直接比较未对齐时间窗口的数据。
28.13 切换与回滚
切换前检查:
1. 增量延迟低于阈值;
2. 数据校验通过;
3. 核心接口压测通过;
4. 回滚开关可用;
5. 监控和告警就绪;
6. 值班人员确认;
7. 业务低峰窗口确认;
回滚方式:
- 配置中心切回旧库;
- 新库写入反向同步旧库;
- 修复分叉数据;
- 冻结新增数据;
- 人工校验后再放量。
回滚必须在设计时验证过,不能在事故现场第一次尝试。
本章小结
分库分表先从业务边界和字段垂直拆分开始,再按稳定高频维度水平分片。分片键决定查询、事务和扩容成本,跨片查询应通过投影、基因或全局索引治理。数据迁移必须包含全量、增量、校验、灰度、切换和反向回滚,不能只做一次性复制。
思考题
- 为什么不能只用行数判断是否分表?
- 分片键应该满足哪些条件?
- 买家维度和卖家维度查询冲突时如何设计?
- 双写切流如何避免数据分叉?
- 写一个分片迁移项目的里程碑和回滚清单。