如何进行分库分表?
核心认知:分库分表不是 MySQL 的调参问题,而是“数据模型 + 路由规则 + 业务改造”的系统工程。
这篇从“要不要拆 → 怎么拆 → 怎么查 → 有啥坑”四个维度来讲。
一、为什么要分库分表?
先说直白一点:不到一定规模,不要轻易分库分表。
一般什么时候才需要考虑:
-
单表数据量太大
- 单表上千万甚至上亿行;
- 索引越来越“高”和“胖”,查询逐渐变慢;
- DDL(加字段、改索引)一动就是几小时。
-
单实例扛不住
- QPS / TPS 明显逼近单机极限;
- CPU、IO 经常打满;
- 即使 SQL & 索引已经优化得差不多。
-
冷热数据明显
- 老数据很少访问,却占着大量存储和索引空间;
- 影响缓存命中率和 buffer pool 利用率。
如果只是偶尔慢、单表几百万行内、SQL 比较随便,那优先做:
- 索引设计;
- SQL 改写;
- 参数调优;
- 加从库做读写分离。
分库分表是真正的“大手术”,要慎用。
二、分库 vs 分表:先把概念整明白
- 分表(Sharding Table):把一个逻辑表拆成多个物理表。
- 例:
order_0、order_1、order_2…
- 例:
- 分库(Sharding Database):把数据分布到多个数据库实例上。
- 例:
order_db_0、order_db_1。
- 例:
通常配套一起用:
逻辑上:order 表
物理上:order_db_0.order_0 ~ order_3
order_db_1.order_0 ~ order_3
更细一点:
- 水平切分(Horizontal Sharding):按“行”切,以某个键做分片;
- 垂直切分(Vertical Sharding):按“列 / 业务模块”切,把不同业务放到不同库。
2.1 垂直拆分(按业务拆库)
思路:
按业务模块把表拆到不同的库里:
- user 库:用户、登录、账户;
- order 库:订单、支付记录;
- product 库:商品、库存。
适用场景:
- 业务边界清晰;
- 模块间关联较少。
优点:
- 架构清晰,各库职责分明;
- 避免“一个实例所有表堆一起”。
缺点:
- 跨库 JOIN 不方便(需要应用层处理);
- 解决不了“单表数据量巨大”的问题。
2.2 水平拆分(按数据行拆库/拆表)
思路:
同一张表的数据分散到多个库/表中:
- 比如按照用户 ID 取模、按时间分表等。
适用场景:
- 单表数据量大;
- 单实例性能瓶颈。
优点:
- 每个分片数据量变小,查询更快;
- 压力分散到多个库实例。
缺点:
- 路由逻辑复杂;
- 分布式事务、跨分片聚合、ID 唯一性等问题需要处理。
三、如何设计分片键和分片规则?
这是分库分表最核心的设计点。
3.1 分片键(Sharding Key)怎么选?
原则:
-
高频查询条件上必须包含分片键
- 大部分查询应该能通过分片键直接定位到“一个分片”;
- 避免“跨所有分片扫一圈”。
-
分片键要有一定的“离散性”
- 避免所有数据堆在一个分片;
- 避免极端倾斜。
-
尽量选“天然存在”的业务字段
- 用户相关业务:可考虑 user_id;
- 商户相关业务:可考虑 merchant_id;
- 订单系统:经常按 user_id / merchant_id 来查时,优先用这些。
不太适合作为分片键的:
status这类取值很少的字段;is_deleted这种布尔字段;- 经常变化的字段。
3.2 分片规则:范围 vs 取模 vs 一致性哈希
常见几种:
1)范围分片(Range Sharding)
按某个范围切:
- 比如按时间:
2024年的订单在一个库/表,2025年在另一个; - 或按 ID 范围:
1~1千万一片,1千万~2千万一片。
优点:
- 便于做“冷热数据分离”(老数据单独放、甚至归档);
- 一些范围查询在同一分片里。
缺点:
- 数据倾斜风险大(热门时间段),容易出现热点分片;
- 扩容时需要做“数据搬家”。
2)哈希取模(Hash / Mod Sharding)
按分片键取模:
db_index = uid % db_count
table_idx = uid % table_count_per_db
优点:
- 数据分布相对均匀;
- 容易实现简单路由。
缺点:
- 扩容时很麻烦:分片数变了,取模结果全乱,要大迁移;
- 范围查询(尤其跨分片的)很麻烦。
可以通过预留分片(预先设较多分片数)来减轻扩容痛苦。
3)一致性哈希 / 虚拟槽
适用一些分布式中间件:
- 用一致性哈希将 key 映射到“虚拟槽”;
- 再将槽映射到物理库表;
- 扩容/缩容时只需调整局部映射,不用全量迁移。
实现较复杂,一般使用现成中间件(如 ShardingSphere、某些云厂商分布式数据库)。
四、分库分表之后,怎么路由 SQL?
4.1 应用层手写路由
最朴素的方法:
- 调用前,根据分片键计算目标库 / 表;
- 通过不同的数据源连接对应库表执行。
伪代码示例:
long uid = getUserId();
int dbIndex = (int)(uid % DB_COUNT);
int tableIndex = (int)(uid % TABLE_COUNT_PER_DB);
String dbName = "order_db_" + dbIndex;
String tableName = "t_order_" + tableIndex;
String sql = "SELECT * FROM " + tableName + " WHERE user_id = ? AND order_id = ?";
优点:
- 可控性高,逻辑完全掌握在自己手里;
- 没有单点中间件。
缺点:
- 业务侵入大,维护复杂;
- 跨表、跨库的 JOIN/统计需要自己写一套框架。
4.2 使用中间件/框架
常见方案:
- ShardingSphere(Sharding-JDBC / Proxy);
- MyCAT 等代理型中间件;
- 各类云厂商的分布式数据库产品。
好处:
- 提供:路由、读写分离、分布式事务支持、跨分片聚合等;
- 应用层可以像操作单库一样写 SQL(有局限,但体验好)。
适合:
- 对自研分片框架成本无暇顾及;
- 业务复杂、开发团队想省心一点。
五、分库分表后的几个大坑
5.1 跨分片 JOIN 问题
原来可以直接写:
SELECT *
FROM order o
JOIN user u ON o.user_id = u.id
WHERE u.id = ?;
分库分表后:
- user 表在 user_db,order 表在 order_db;
- 甚至 order 还按 user_id 水平拆到了多个表;
- 跨库 JOIN 基本直接废掉。
常见解决方案:
-
冗余字段
- 在 order 里冗余 user 的部分字段(如 user_name);
- 用于展示时不再 JOIN user 表。
-
应用层分步查询
- 先查 user;
- 根据 user_id 再去对应分片查 order;
- 手工在内存中做结果组合。
-
通过中间件支持分片 JOIN
- 中间件在后台拆分 SQL、多路查询、做 merge;
- 适合数据量较小的 JOIN 场景。
5.2 分布式事务问题
跨库操作时:
库 A:减库存
库 B:创建订单
传统单库事务无法覆盖,需要考虑:
- 2PC(两阶段提交)——复杂且性能一般;
- TCC、可靠消息、最终一致性等分布式事务方案。
常见做法:
- 能在同一个分片解决的事务,就不要跨分片:
- 分片键设计时尽量让“事务内的多个表”落在同一分片;
- 对少数必须跨分片的场景:
- 使用业务层“补偿机制”的最终一致方案。
5.3 全局唯一 ID 问题
分库分表后:
- 自增主键(AUTO_INCREMENT)只能保证“单库单表内自增”;
- 无法保证“全局唯一 + 有序”。
常见解决方案:
-
雪花算法(Snowflake)
- 格式一般是:时间戳 + 机器 ID + 序列;
- 有序、全局唯一;
- 不能保证连续。
-
号段发号器(Segment)
- 单独建一张“号段表”;
- 应用一次取一段 ID(如 1000 个),在内存中发号;
- 高可用 + 批量减少数据库压力。
-
数据库序列表
- 维护一个 sequence 表,每次
UPDATE+LAST_INSERT_ID; - 并发高时压力集中,容易成为瓶颈。
- 维护一个 sequence 表,每次
5.4 跨分片统计和分页
比如要做:
统计所有订单总量、总金额
查最近 10 条订单(全局排序)
分片后,数据被打散:
- 单次查询难以直接从某一个库/表拿到全部结果;
- 跨分片全量统计 = 多分片聚合;
- “全局分页” = 多分片取 TopN 再合并排序。
常见方案:
- 使用中间件提供的分布式聚合功能(ShardingSphere / MyCAT);
- 分场景处理:
- 常用统计维度提前做 汇总表 或 离线数仓;
- 复杂统计走大数据/OLAP 系统(如 ClickHouse、Hive、Spark);
- 线上 MySQL 分片只负责“在线交易数据”。
六、如何平滑从单库迁移到分库分表?
这一步很关键,不能“停服一天导数据”。常见迁移路径:
6.1 双写 + 验证
-
第一阶段:只写老库,读老库
- 开发分库分表逻辑;
- 写一套“同步任务”从老库同步到新分片结构(可用 Canal / binlog 方案);
-
第二阶段:新旧双写
- 应用写请求同时写老库 & 新分片库;
- 读请求仍然走老库;
- 对比新旧数据是否一致。
-
第三阶段:读新库,写新库
- 逐步把读请求切到新库;
- 观察一段时间;
- 确认稳定后逐步退役老库。
6.2 短暂停写 + 全量迁移(小规模场景)
-
对于体量较小、业务可短暂停机的系统:
- 停机 → 阻塞写入;
- 全量导数据到新分片;
- 切换应用连接;
- 恢复服务。
-
一般只适合内部系统或低峰期窗口。
七、一个简单的分库分表示例(按 user_id 取模)
假设:
- 2 个库:
order_db_0、order_db_1; - 每个库 4 张表:
t_order_0~t_order_3。
7.1 分片规则
dbIndex = user_id % 2
tableIndex = user_id % 4
database: order_db_{dbIndex}
table: t_order_{tableIndex}
7.2 建表示例
-- order_db_0.t_order_0
CREATE TABLE t_order_0 (
id BIGINT PRIMARY KEY, -- 全局唯一 ID(雪花算法)
user_id BIGINT NOT NULL,
amount DECIMAL(10,2),
status TINYINT,
create_time DATETIME,
KEY idx_user_ctime(user_id, create_time)
) ENGINE = InnoDB;
-- 其它 7 张表结构一致,只是表名不同
7.3 应用层路由伪代码
long userId = getUserId();
int dbIndex = (int)(userId % 2);
int tblIndex = (int)(userId % 4);
String dbName = "order_db_" + dbIndex;
String tableName = "t_order_" + tblIndex;
String sql = "SELECT * FROM " + tableName + " WHERE user_id = ? AND id = ?";
八、分库分表的 Checklist(可以贴墙上)
-
确认真的需要吗?
- 单表数据量 > 2000 万?
- 单实例已经调优到极限?
- QPS / TPS 常年高位?
-
先做垂直拆分,再做水平拆分
- 按业务拆库 → 单表变少;
- 再对热点大表按行拆分。
-
分片键选好了没?
- 覆盖主要查询场景?
- 分布是否均匀?
- 尽量保证“一个事务内的表”在同一分片?
-
分片规则明确了吗?
- 范围 / 取模 / 一致性哈希?
- 扩容时如何调整?
-
跨分片问题方案:
- JOIN → 冗余字段 + 应用层拼装;
- 事务 → 尽量局部分片 + 补偿机制;
- 统计 → 汇总表 / 数仓 / 中间件。
-
全局 ID 方案定了吗?
- 雪花算法 / 号段发号器 / 其他方案?
-
迁移方案准备好了吗?
- 双写 + 校验?
- 数据回滚预案?
- 监控和压测?
九、小结
分库分表的关键不是“语法怎么写”,而是:
- 到底要不要拆?
- 怎么根据业务特征选分片键和规则?
- 拆了之后,路由、ID、事务、JOIN、统计怎么搞?
- 如何安全地从单库切到分片架构?
一句话:
把分库分表当成“架构升级”,而不是简单的 MySQL 优化。
想清楚业务访问模式、流量结构和增长预期,然后再动刀,会少掉非常多的坑。
1156

被折叠的 条评论
为什么被折叠?



