分库分表:拆之前先想清楚这三件事,否则早晚要还债
很多团队把分库分表当成“高并发万能药”。单库扛不住了,拆;慢查询多了,拆;连接池满了,拆。拆完发现,跨库JOIN没法写,分布式事务搞不定,数据迁移差点丢数据——这才是噩梦的开始。
分库分表是“终极手段”,不是“首选方案”。动手之前,先回答三个问题:拆什么、怎么拆、拆完怎么迁。
第一件事:拆之前,先确认“必须拆”
一个真实案例:某电商订单系统,高峰期单库写入TPS约5000,读QPS约2万,单库MySQL CPU已到80%。但排查发现,慢查询占60%——索引没建对,缓存命中率极低。团队花了三周优化索引和缓存,CPU降到40%,撑过了大促。
判断“必须拆”的标准很简单:
- 数据量超过单库容量上限(比如MySQL超过2TB,备份和恢复时间不可接受)
- 单库写入TPS持续超过3000-5000,且优化后仍无法下降
- 单表数据量超过2000万行,且索引维护成本过高
这三条都不满足,别拆。拆了等于给自己找事。
第二件事:拆的策略——选对分片键比选对中间件更重要
分片键选错,后患无穷。最常见的错误是:拿订单ID做分片,但业务查询都是按用户ID查的。结果每个查询都要路由到所有分片,性能比单库还差。
分片键选择的铁律:选“高频查询”的维度。 用户下单查订单,高频维度是用户ID,那就按用户ID分片。订单号作为分片键只在“按订单号查”场景高效,但电商系统里“查某用户的订单列表”才是高频。
分片算法有三个常见选项:
1. 哈希取模
shard_id = hash(user_id) % 16
优点:数据分布最均匀。缺点:扩容时几乎要全量迁移。16个库扩到32个,所有数据都要重新分布。
2. 范围分片
shard_id = user_id / 1000000 // 每100万用户一个库
优点:扩容方便,直接加库,已分配的数据不动。缺点:热点问题——新用户集中在最新分片,老分片空闲。
3. 一致性哈希
ring_position = hash(user_id) // 落到哈希环上
优点:扩容只影响相邻节点,迁移量小。缺点:实现复杂度高,且依然有热点可能。
实战选择建议:
- 用户量可预估、增长平稳 → 哈希取模,简单可靠
- 用户量爆发式增长、数据倾斜不明显 → 范围分片,扩容方便
- 数据规模极大、分片数动态变化 → 一致性哈希,但一般团队没必要
一个容易踩坑的地方: 哈希取模的分片数,最好选质数(如17、31),不要选2的幂。选2的幂,哈希低位分布不均,容易导致数据倾斜。这个坑不踩不知道,踩了数据分布一看就傻眼。
第三件事:平滑迁移——最容易被低估的环节
拆完库,数据还在老库。怎么迁?停机迁移最简单,但业务不允许。平滑迁移的方案,业内叫“双写迁移”,分三步走:
步骤1:老库和新库并行运行,业务写入同时发到两个库
步骤2:用数据同步工具(如Canal)把老库历史数据迁移到新库
步骤3:校验数据一致性,确认无误后,切换读流量到新库,最终下线老库
关键点有三处:
关键点1:双写的顺序。先写老库,再写新库。如果新库写失败,不影响主流程,只需要记录日志,后续异步补偿。反过来先写新库,老库写失败,主流程直接报错。
关键点2:历史数据迁移的校验。数据同步工具迁移完成后,必须做数据对账。最简单的做法:对每个分片,分别count老库和新库的行数,不一致就查差异。但count大表很慢,更实际的办法是抽样校验:按ID范围抽5%,比对字段值。
关键点3:切换流量要灰度。别一次性把全部读流量切到新库。先切5%的流量,观察慢查询和错误日志,确认稳定后再逐步提升到10%、50%、100%。这个过程可能需要几天,但安全第一。
这个方案的核心代价是:业务代码需要维护双写逻辑,相当于每个写操作要发两次。代码复杂度上升,且需要处理双写失败时的补偿。但这是平滑迁移的必然代价——不想停机,就得接受这段“脏活”。
演进路线:别想着一步到位
分库分表不是一次性动作,它是一条演进路径:
阶段1:单库单表 → 优化索引、加缓存,撑到极限
阶段2:单库分表 → 拆表不拆库,缓解单表压力
阶段3:分库分表 → 数据量再涨,库也拆开
阶段4:引入中间件 → ShardingSphere或MyCat,屏蔽分片路由细节
阶段5:单元化 → 按用户维度垂直拆分,每个单元独立部署
阶段2是很多团队忽略的过渡方案。 如果数据量过千万,但TPS不高,先拆表不拆库,成本低,不需要引入中间件,代码改动也小。等单库连接数或IO成为瓶颈,再走阶段3。
一个建议: 不要一开始就上中间件。中间件(如ShardingSphere)虽然屏蔽了分片细节,但也带来了新的复杂度:分布式事务、全局ID生成、跨分片聚合查询,都成了新问题。如果团队规模不大,能用手写分片逻辑(通过一个路由层封装)就先用着,等分片数超过8个再考虑中间件。
拆完之后的“债”怎么还
拆完分库分表,有些问题绕不开:
- 跨分片JOIN:基本别想了。要么冗余字段,要么反规范化设计,要么在应用层做聚合。
- 分布式事务:用本地消息表+最终一致性方案,比强一致方案(如XA)更实用。电商订单场景下,订单创建和库存扣减,最终一致就够了,不需要强一致。
- 全局ID:用雪花算法(Snowflake),别用自增ID。自增ID在分片下无法保证全局唯一。
这些问题在拆之前就得想明白。拆完再想,代码已经写死了,改造成本翻倍。
一句话收尾
分库分表解决的是“单库物理极限”,不是“慢查询”;拆之前先优化索引和缓存,拆的时候选对分片键,拆之后平滑迁移——这三步走稳了,分库分表才能成为架构的加分项,而不是负债项。
下一步做什么: 拿你手头最慢的那张表,先做索引分析和缓存优化,跑一周看看效果。如果还扛不住,再画分片方案图,按本文的三步走评估一次。
本文关键词:分库分表、分片键、平滑迁移、双写、哈希取模