Skip to content

分库分表 ​

单表数据涨到千万级,查询开始变慢。加索引、加缓存都试过了,还是扛不住。这时候考虑把一张大表拆成多张小表,分散到多个库。分库分表要回答的核心问题是,什么时候必须拆,按什么键拆,以及拆完要承受什么代价。

1. 单表什么时候到瓶颈 ​

单表变慢的本质不是数据量本身,是索引变深加数据页变多。一张千万行的表,B+ 树索引高度上升,每次查询要多访问几层。即使走索引,命中的行如果散落在大量数据页里,随机 IO 次数也会上升。

但这不是拆表的充要条件。很多人一看到几百万行就想着分库分表,这是过早优化。先问三个问题,索引建对了没有,SQL 有没有走索引,热点数据能不能用缓存挡掉。这三个都没解决,拆表是逃避问题而不是解决问题。

拆表的真实触发条件是,单表数据量到了亿级,或者单库的写入 QPS 和存储容量到了物理上限,加索引、加缓存、加从库都顶不住的时候。在此之前,分库分表带来的复杂度远大于收益。

2. 拆分键是命门 ​

决定拆表后,第一个也是最重要的决策是选拆分键。拆分键决定了数据按什么维度分散,也决定了哪些查询还能在一个库内完成。

以订单表为例。按 user_id 拆,一个用户的所有订单在同一个分片里,查「我的订单」不需要跨库。但按 user_id 拆了之后,查「某商品的全部订单」就要跨所有分片。反之,按 order_id 拆,单条订单查询很高效,但按用户或按商品聚合查询全部要跨库。

所以拆分键没有最好的,只有最合适的。选它的依据是,你的业务里最高频、最不能慢的那个查询维度是什么,就按那个维度拆。拆完其他维度的查询变慢,用冗余或异步聚合来补。

3. 拆分的代价 ​

分库分表不是免费的性能提升,它砍掉了一堆单库时代理所当然的能力。

跨库 join 没了。订单表和用户表分属不同库,一条 join 查不出「订单加用户名」,要么冗余用户名到订单表,要么查两次在应用层拼。

全局自增 ID 没了。多个库各自自增会撞车,必须引入雪花算法或号段模式的分布式 ID,见 分布式 ID 生成。

分页变复杂了。跨分片分页要先把各分片的前 N 条取出来,再在应用层归并排序,offset 越大越慢。

扩容要迁数据。分片数定了之后,加新分片意味着所有数据要重新按新规则哈希搬迁,这是个大工程。

这些代价合起来,分库分表更像一次架构重构,而不是一次性能调优。所以判断要谨慎,单表没到物理上限,不要动这个念头。

4. 跨分片分页 ​

分页在单库时代是 LIMIT offset, size 一条语句的事,拆表后变成跨多个分片取数据再合并。它比单库分页多两件事,取和并,两件事各有一个绕不开的坑。

4.1 深分页的代价 ​

跨分片分页的通用做法是全局归并,要第 N 页的数据,每个分片都取前 offset + size 条,汇总到应用层归并排序,再截取第 N 页那一段。这条路的数据量和偏移量成正比,offset 越大,每个分片要捞的数据越多,内存和网络开销线性增长。翻到第 1000 页时,几乎等于把全表都捞出来排了一遍。

归并本身还依赖排序键能唯一定位,否则相同排序键的行在截取时会被丢或重复。所以分页的排序条件里要带一个唯一键兜底,比如 order_id。

4.2 用游标替代 offset ​

深分页的根子是 offset,它要求数据库从头数到第 offset 条。换成游标分页(keyset)就不一样,不传页号,传「上一页最后一条的排序键」,下一批直接从这个键往后取。

sql
SELECT * FROM t_order
WHERE user_id = ? AND create_time < ? AND order_id < ?
ORDER BY create_time DESC, order_id DESC
LIMIT 20;

每个分片都能直接定位到游标之后的位置,不用从头数,offset 再大代价都不变。代价是只能一页页往下翻,不能跳页,也拿不到总页数。

结论是,分库分表后的分页能用游标就用游标。游标覆盖不了的产品形态(要跳页、要看总页数),再退回全局归并,但把可翻的深度限制住,比如只允许翻前几十页。

5. 拆表和分库是两回事 ​

分表解决单表数据量过大的问题,一张表拆成多张,可以还放在一个库里。分库解决单库写入和容量上限的问题,多张表分散到多个库实例。

两者的触发条件不同。单表数据量大、但单库还能装下,只分表就够了,不用分库。只有单库的 IO、连接数、容量也到顶了,才需要分库。

很多方案是先分表,数据继续涨再分库。一上来就分库,是把两个问题混为一谈,白白引入跨库的复杂度。

6. 结论 ​

分库分表的判断顺序是,先确认索引、缓存、从库都用尽了,再确认单表到了亿级或单库到了物理上限,然后才动手拆。拆之前把拆分键选对,选错键意味着最高频的查询也要跨库。拆完要接受 join 消失、ID 要重造、分页要归并、扩容要迁数据这四笔账。

7. 相关章节 ​

拆库后的读写路由见 读写分离,拆表后的分布式 ID 见 分布式 ID 生成。多租户场景下的数据隔离是另一个维度的问题,见 多租户数据隔离。

Released under the MIT License.