单表数据量从几十万涨到几千万以后,很多人会发现原本一两百毫秒的查询逐渐变成几秒,加索引、优化 SQL 之后仍不稳定,于是开始把目光投向"分库分表"。分库分表确实是应对数据量增长的一条路,但它不是免费的,落地之后会带来跨分片查询、分布式事务、扩容迁移等一系列新问题,处理不好的话,复杂度会比原来的性能问题更让人头疼。
这篇文章讲清楚三件事:分库分表到底在拆什么;什么量级、什么信号出现时才真正需要它;如果决定要做,分片键、中间件、扩容这些关键决策应该怎么考虑。文章面向中小企业的运维和开发,给出的数字都是经验参考,不是硬性标准,实际方案一定要结合业务读写模式和压测数据来定。
垂直拆分与水平拆分
分库分表并不是一种单一操作,它包含两个维度:垂直方向和水平方向。理解这两个维度,是判断该不该拆、怎么拆的前提。
垂直拆分:按业务或按字段切
垂直拆分又分两种情况。第一种是垂直分库,按业务模块把不同表拆到不同的库。比如一个电商系统,把用户相关的表放在用户库,商品相关的表放在商品库,订单相关的表放在订单库。这种拆法的好处是业务边界清晰、每个库的负载相对独立,某个业务写入压力大时可以单独扩容。它本质上就是服务化拆分在数据层的对应动作,通常和微服务改造一起做。
第二种是垂直分表,把一张宽表按"冷热字段"拆成多张表。比如商品表里既有被频繁查询的标题、价格、状态,又有体积大、很少读取的详情描述、参数说明。可以把热字段留在主表,冷字段拆到扩展表,主表变窄之后单行数据变小,同样的缓冲池能缓存更多行,扫描也更快。垂直分表更像是一种优化手段,代价是查询完整对象时需要多一次关联或多次查询。
水平拆分:按行分散到多个分片
水平拆分指的是同一张表的数据,按某种规则分散到多个库或多个表里。每个分片保存的是一部分行,所有分片的表结构完全相同。比如订单表按用户 ID 取模,用户 ID 除以 4 余数为 0 的订单落在分片 0,余数为 1 的落在分片 1,以此类推。对外看起来还是一张逻辑上的订单表,但物理上已经是四个独立的表甚至四个独立的库。
水平分库是把分片放到不同的数据库实例上,能分散单机的 CPU、内存和 IO 压力;水平分表是在同一个实例内把一张大表拆成多张小表,主要解决单表过大导致的索引维护和查询效率问题。两者可以叠加使用,比如先分成四个库,每个库里再分成十张表,总共四十个物理分片。
两种拆法的对比
| 维度 | 垂直拆分 | 水平拆分 |
|---|---|---|
| 拆分依据 | 业务模块或字段冷热 | 数据行按分片键分布 |
| 解决的问题 | 业务耦合、单表过宽 | 单表数据量过大、单机写入瓶颈 |
| 结构变化 | 库或表结构不同 | 所有分片表结构相同 |
| 跨片问题 | 需要跨库关联 | 需要处理路由与聚合查询 |
| 扩容难度 | 相对简单 | 需要数据重分布,较复杂 |
| 典型场景 | 微服务改造、宽表优化 | 订单、日志、消息等海量表 |
什么时候才真的需要分库分表
这是最容易踩坑的地方。分库分表有人力和复杂度成本,过早引入往往得不偿失。更常见的做法是先穷尽成本更低的办法:优化索引、优化慢 SQL、做归档、用分区表、加只读副本、上缓存。
两条触发信号
第一条是单表数据量信号。经验上,InnoDB 单表行数超过两千万到五千万、并且索引深度增长导致查询开始明显变慢时,可以考虑分表;这只是一个粗略参考,真正决定性能的是行大小、索引数量、访问模式以及缓冲池能否装下活跃数据。如果一张两千万行的表,绝大多数查询都能命中索引并且只返回几十行,它可能一点都不慢;反过来一张八百万行的表,如果全是全表扫描,也可能已经撑不住。
第二条是写入瓶颈信号。当单机磁盘的写入 IOPS 持续打满、主从复制延迟不断增大、单实例 CPU 被写事务占满,且已经做了表结构优化仍然无法缓解,这时单机写入能力确实到了天花板。另一种常见信号是单库连接数长期接近上限、大表加索引或改表结构时锁表时间难以接受。
先用代价更低的替代方案
| 替代方案 | 适用情况 | 成本 |
|---|---|---|
| 索引与 SQL 优化 | 查询慢但数据量不算大 | 低,应最先做 |
| 历史数据归档 | 冷数据占比高、只查近期数据 | 低到中 |
| 分区表 | 按时间查询、可整体删除旧分区 | 中,应用改动小 |
| 只读副本 | 读多写少 | 中 |
| 缓存 | 热点数据重复读取 | 中,需处理一致性 |
| 分库分表 | 单表巨大且写入压力持续 | 高,改动大 |
很多所谓"必须分库分表"的场景,做完归档和索引优化之后其实就回过神来了。数据是持续增长的历史包袱,把半年前、一年前的数据迁到归档库,主表只保留热数据,往往能直接把单表规模降一个数量级。
分片键怎么选
水平拆分的核心是分片键,也就是决定一行数据落到哪个分片的字段。选错分片键,后面几乎无法挽回,只能重做。
几条基本原则
第一,分片键要能落到绝大多数查询条件里。如果订单按用户 ID 分片,那么几乎所有查询都应该带用户 ID;如果存在大量按订单号查询的场景,而订单号并不包含用户 ID,路由就无法定位分片。第二,分片键的取值要足够离散,避免数据倾斜,比如用性别、状态这类低基数字段分片,数据会严重不均。第三,分片键要尽量不可变,如果主键或分片键会被更新,就会出现"数据从一个分片搬到另一个分片"的麻烦。
常用的分片规则有取模、范围、哈希、一致性哈希。取模分布均匀但扩容时几乎要全量搬迁;按范围(比如按时间)便于扩容和历史归档,但容易出现热点分片;哈希兼顾均匀度,但同样面临扩容迁移问题;一致性哈希能减少扩容时的搬迁量,但实现和运维相对复杂。选哪一种是典型的权衡,没有银弹。
常见分片策略对比
| 策略 | 数据分布 | 扩容迁移量 | 热点风险 | 适用场景 |
|---|---|---|---|---|
| 取模 | 均匀 | 大 | 低 | 分片数长期固定 |
| 范围 | 依赖取值 | 小 | 高 | 时序数据、归档友好 |
| 哈希 | 较均匀 | 大 | 低 | 需要打散分布 |
| 一致性哈希 | 较均匀 | 较小 | 中 | 弹性扩容需求强 |
中间件与实现思路
要让应用像访问单库一样访问分片集群,通常有两种落地路径:引入分库分表中间件,或者应用层自己做数据源路由。
中间件的两种形态
一类是客户端 SDK 形态,也就是以 jar 包方式嵌入应用,应用通过它来解析 SQL、计算路由、分发到后端库,代表思路是 Apache ShardingSphere 的 JDBC 模式。另一类是代理层形态,中间件本身是一个独立进程,应用像连普通 MySQL 一样连到代理,由代理负责分片和聚合。前者的好处是少一跳网络、配置灵活;后者对应用近乎透明、支持多语言,但多了一层网络,代理本身也可能成为瓶颈和单点。
MyCat、ShardingSphere-Proxy 都属于代理层思路,ShardingSphere-JDBC 则属于客户端思路。具体功能、版本支持和配置方式差异较大,落地时一定要以对应项目的官方文档为准,不要照搬网上的二手配置。
跨分片查询与分布式事务
分库分表之后,最麻烦的是跨分片操作。跨分片查询会出现两类问题:一是无法定位分片,只能把查询广播到所有分片再归并结果,延迟和资源消耗都明显上升;二是分页、排序、聚合需要在中间层做二次处理,比如 ORDER BY 加 LIMIT 时,要从每个分片各取一部分再全局排序,数据量大时非常耗资源。
分布式事务更难。跨库的强一致事务无法靠单个 MySQL 事务完成,常见方案有基于 XA 的两阶段提交、基于消息的最终一致、以及 TCC 等。XA 实现相对标准但性能和可用性有代价;消息方案需要业务能接受最终一致,并且要处理消息幂等和补偿。原则是能不分片到两个库、就不要跨库写;能通过合理的分片键让同一业务实体的数据落在一个分片、就尽量落在一起。
扩容与数据迁移
分库分表一旦上线,数据量还会继续增长,迟早要面对扩容。扩容的核心难点是从旧分片数迁移到新分片数时,如何在不长时间停机的前提下搬数据并保证一致。
大致的迁移流程
思路一般分几步:先部署新分片,开启双写或通过 binlog 增量同步把新数据也写到新分片;然后做历史数据的全量搬迁;全量加增量追平之后,进行数据校验;校验通过再切换读流量到新分片,最后切换写流量并下线旧分片。整个过程要保留回滚路径,每一步都要能独立回退。
相关的命令可以先用工具查看表和分片状态,示例仅作演示,具体以实际环境为准:
# 查看 MySQL 中当前表的大小与行数估计(近似值)
mysql -uroot -p -e "SELECT table_schema, table_name, table_rows, \
ROUND(data_length/1024/1024,1) AS data_mb, \
ROUND(index_length/1024/1024,1) AS index_mb \
FROM information_schema.tables \
WHERE table_schema='shop' ORDER BY data_length DESC;"
# 查看 binlog 当前位点,为增量同步做准备
mysql -uroot -p -e "SHOW MASTER STATUS\G"
# 查看正在进行的复制状态(在从库执行)
mysql -uroot -p -e "SHOW SLAVE STATUS\G"
需要提醒的是,table_rows 来自统计信息,可能不准,做容量规划时要以实际 COUNT 抽样或使用更精确的方式确认。迁移期间对源库的读取会带来额外压力,建议放量控制,避免影响线上业务。
扩容的注意事项
一致性校验是迁移里最容易被忽视的一环。行数相等不代表内容一致,需要比对校验和,或者按业务主键分段抽样比对。切换前要明确"读在新写还在旧"和"读写都在新"两个阶段的顺序,先切读观察一段时间,确认无误再切写。旧分片至少保留一个可回滚的窗口期,确认稳定后再清理。
常见问题
单表到多少行就必须分表?
没有固定数字。常见经验值是两千万到五千万行开始评估,但真正要看查询是否能高效命中索引、活跃数据能否放进缓冲池,以及写入是否已成为瓶颈。
分库分表之后还需要索引吗?
需要,而且更需要。分片减少了单表数据量,但每个分片内部依然要靠索引来定位行。分片键上通常要建索引,路由查询才高效。
用中间件会不会让 SQL 变慢?
会有一部分开销,代理层多一跳网络和一次 SQL 解析,跨分片查询还要归并结果。能通过分片键精确定位到单个分片的查询,开销很小;广播查询则会明显变慢。
分片键选错了能改吗?
能改,但代价很高,本质上等同于一次全量数据重分布。所以分片键设计一定要在动手之前反复推演,覆盖全部核心查询路径。
一定要分库吗?只分表行不行?
只分表可以先把单表变小,缓解索引维护和大表 DDL 的问题,但无法突破单机 CPU、内存和 IO 的上限。如果瓶颈在单机资源,只分表不够,需要分库。
总结
- 分库分表分垂直和水平两个维度,垂直按业务或字段拆,水平按行分散,两者常叠加使用。
- 决定拆分前先穷尽更低成本的方案:索引优化、SQL 优化、数据归档、分区表、只读副本和缓存。
- 触发信号主要是单表数据量过大和单机写入持续打满,具体阈值要结合行大小、访问模式和缓冲池判断。
- 分片键是设计的核心,要能覆盖主要查询、取值离散且尽量不变,取模、范围、哈希各有取舍。
- 落地方式有客户端中间件和代理层两条路,跨分片查询和分布式事务是最大难点,能避免就避免。
- 扩容本质是一次数据重分布,要靠双写或增量同步加一致性校验来缩短停机,并始终保留回滚路径。
企业QQ咨询




