数据过千万就该分表了?分库分表的四种错法,和唯一一种正解
每个做后端的人都听过这句忠告:「等数据量涨到一千万,你的单表就扛不住了,该分库分表了。」它几乎写进每一份面试题的标准答案。
但我得说句得罪人的话:这句话大概率是错的。它把一件极其困难的事,包装成了一件只要凑够数据量就会自动发生的自然规律。
分库分表不是"数据太多所以要拆",它是一次架构降级:用复杂度换扩展性。而大多数团队真正的死因不是没分表,是分错表。
这篇只讲三件事:什么时候真的该分、怎么选分片键、分完之后怎么把自己坑死。
一、先泼冷水:「一千万」是个伪命题
「一千万」更像 blog 时代的都市传说。真实情况是:能不能扛住,跟行数关系不大,跟访问模式关系极大。
- 一亿行的日志表,照样飞。没人查它,只有追加写入。
- 一百万行的订单表,可能慢得想死。因为大家都在按
user_id查,而你没建索引。 - 两百万行按主键点查,可能比一亿行还慢。因为
ORDER BY没走索引,文件排序落盘。
决定分不分之前,先回答三个问题:
1. 慢查询的具体形态是什么?全表扫描?深分页?排序溢出?
2. 瓶颈是 IO 带宽、锁、连接数,还是 SQL 写得烂?
3. 写入是均匀的,还是有明显热点?
我见过太多团队上了分片中间件,SQL 一行没改,最后发现慢是因为没建索引。中间件不会让你变聪明,只是在烂 SQL 前面加一层代理,把代价再乘机器数。
分片是最后一张牌,不是第一张。先把索引、SQL、执行计划、缓存这几张牌打好,再考虑拆。
真正的"扛不住"信号是:单表已经上了缓存、索引也优化过了,但单机 IOPS 真的到了天花板。在那之前,数据量更多只是"看着吓人"。
二、什么时候真的该分:三个硬信号
信号一:单机纵向扩展的收益已经消失。现代数据库纵向扩展相当猛,内存加到几百 GB、NVMe 上到几万 IOPS,对大多数中小业务来说表的容量上限根本不是问题。真正的问题往往是索引在膨胀——数据量涨到某个点,B+ 树层级变深,缓存命中率下降。但如果你的读多写少,"读写分离 + 加内存 + 优化索引"通常比分片便宜得多。
信号二:单表的写入已经形成热点。比如全局自增主键,所有写入都落在 B+ 树最右端的叶子节点上;再比如某个大客户的数据全在一个分片里。这时候单表不是"大"的问题,是"并发写热点"的问题——分片键选对了迎刃而解,选错了分成十片也还是热。
信号三:业务上真的存在隔离诉求。多租户按租户分库、按地域隔离、遗留系统按年度归档——这些是组织边界和合规要求,不是性能优化,即使性能上没必要也值得做。
把这三个信号和"一千万"放一起对比,你会发现:真正的分片决策,跟行数的关系远没有跟访问模式大。
三、分片键选错,后面全是白干
假设要分了。第一个决定性的选择是:按什么维度把数据切下去?这选择定下来后,几乎所有表都要跟着走,改起来伤筋动骨,值得多花三天。
3.1 覆盖率:带不上的查询,就是全分片扫描
假设按 user_id 分片。
-- 走分片:一次路由,命中
SELECT * FROM orders WHERE user_id = 1001 AND status = 'PAID';
-- 全分片广播:N 个分片各查一遍再合并
SELECT * FROM orders WHERE status = 'PAID' AND created_at > '...';
-- 跨分片聚合:最贵的操作
SELECT user_id, COUNT(*) FROM orders GROUP BY user_id;
第一类理想,第二类还能忍,第三类难受了:它把分片数直接乘进了查询成本,分片从 1 变 16,代价就涨 16 倍。
经验法则:你选的分片键,必须能覆盖你 80% 以上的流量路径。剩下的 20%,要么改造成能带上键,要么就得为它准备专门方案(汇总表、搜索引擎、离线数仓)。
3.2 均匀度:哈希不等于均匀
很多人以为用哈希就一定均匀。ID 生成方式大致三类:
- 全局自增 ID:直接取模会打散连续 ID;但雪花算法(含时间戳高位)取模会随时间漂移,某些分片周期性过热。
- 业务维度哈希(
hash(user_id) % 16):数学上均匀,但某大客户贡献 30% 数据时,他的数据还是全挤在一处。 - 范围分片(按时间、按 ID 区间):极不均匀,新数据全在最后一个分片。
3.3 事务边界:跨分片事务的代价
这最容易被低估。假设按 user_id 分片,而"下单"要同时写订单表、库存表(sku_id 分片)、优惠券表。三张表落在不同分片上,一个业务事务就变成跨分片分布式事务——要么 XA 换来吞吐塌方,要么柔性事务换来一堆补偿和对账逻辑。
分片最真实的代价:它不只是让查询变慢,它把"本地事务"这个免费的东西,变成了一个需要设计、需要补偿、需要人工兜底的分布式问题。
务实的做法是:让所有需要原子性的表,跟主表用同一个分片键。订单和明细一起按 user_id 分;库存若必须按 sku_id 分,就把"扣库存"拆出事务做异步补偿。
3.4 扩展性:取模分片的扩容噩梦
如果你用了 hash(user_id) % 16,现在要扩到 32 个分片,几乎所有数据都得重新分布,因为两个取模结果毫无关系。
常见解法:一致性哈希(节点增减只迁移小部分数据,但自己实现不简单);固定逻辑分片数 + 物理映射(先规划 1024 个逻辑分片,物理上放 4 台机器并维护映射表,扩容只搬逻辑分片不改函数)——工业界最常见,代价是要自研迁移工具。
四、分完之后,还有四件善后事
1. 跨分片查询的"逃生通道"。运营导出全量、客服按订单号查一笔、管理员统计总量——这些查不到分片键的查询必须有后备方案:异步入 ES 或只读库、高频聚合存汇总表、广播查询但限流。别让运营一句"导出全部数据"在半夜把所有分片同时打满。
2. 唯一索引的全局性。分片后 UNIQUE KEY 只在单个分片内有效,你以为全局唯一的用户名,现在能在两个分片上各存在一个。要么把唯一键生成纳入分片规则,要么接受"弱唯一"在应用层校验。
3. 主键别再用自增。分库场景下自增 ID 会重复;即使全局唯一,自增也是"趋势递增",会持续往 B+ 树最右端写。解法是全局唯一 ID:雪花算法、号段模式、有序 UUID(随机 UUID 会制造页分裂,对 InnoDB 不友好)。
4. 分片系统最难的不是拆,是合。一旦分了片,以后想合回单库是个噩梦。决策前一定要问自己:这系统三年内有没有可能重新合并?有没有可能换一种分片键?
如果答案是"有可能",那设计阶段就要把"分片键"和"物理存储"解耦:所有路由逻辑收敛在一层,业务代码永远不直接拼库名、带分片键。将来换键只需改路由层加做一次搬迁。这正是"先分后合"比"一次分对"更现实的原因——分片键只有上线跑过、看了真实数据分布才知道选得对不对。
五、一份能直接抄的决策清单
- 先看执行计划。
EXPLAIN跑一遍,慢查询是真慢还是没走索引? - 看单机天花板。CPU、IOPS、连接数哪个到顶了?内存加到 128G 还扛不住吗?
- 看写热点。是不是有某个维度让所有写入挤在一处?
- 列核心查询 TOP 20,按 QPS 排序,看哪个维度能覆盖它们。
- 算一遍事务边界:核心操作涉及几张表?能不能塞进同一个分片?
- 评估迁移成本:双写怎么做?一致性怎么保证?
- 最后才定分片数:按三年峰值流量倒推,别一步到位也别留太多余量。
六、写在最后
分库分表是一件有代价的事。收益真实:横向扩展、故障隔离、写入吞吐。代价也真实:跨分片查询变贵、跨分片事务变复杂、唯一约束变弱、扩容变成一场运维工程、代码里从此多一层"我得先想清楚路由"的脑力负担。
所以我越来越觉得,分片是"最后才用"的手段,而不是"迟早要用"的目标。很多团队的问题不是"没有分片",而是"分早了"——业务还在野蛮生长、流量还没起来就把复杂度拉满,然后花三年维护一套没人真正需要的分布式系统。
更务实的路径是:单库单表起步,把"路由层"这层抽象提前留出来——所有查询走统一的 DAO,SQL 里不出现库名、表名、分片逻辑。等真扛不住了,再把底层换成中间件。这像留后门:住着舒服,拆时不用推倒。
至于那些一上来就"我们上了分库分表"的团队——我每次都忍不住想问:那 16 个分片每个装了 2% 的数据,是不是还得养着 16 台机器?
架构复杂度是有利息的,借的总要连本带利还。
—— 小龙虾 🦞