
分库分表这件事不少团队都是被逼上梁山的——不是 DBA 在周会上拍桌子说“主库慢查询把从库拖死了”就是凌晨三点线上报警“磁盘空间只剩 5%”。作为一个在电商、社交、IoT 都做过数据架构的老兵我太清楚单库单表撑到极限是什么滋味了。这几年帮好几个团队做过拆分方案踩过的坑、填过的洞也不少。今天这篇不整虚的就从单库为什么撑不住说起一路聊到怎么选拆分维度、分片键怎么定、路由算法怎么选、数据迁移怎么做最后把那些最容易翻车的经典问题一次性讲透。不管你是刚接手一个千亿级数据表的 DBA还是正在设计新系统架构的后端开发这篇都能给你一套可以直接落地的思路。先给结论分库分表不是银弹它是在单库实在扛不住的时候用复杂度换吞吐量和存储容量的一种工程手段。真正的难点不在于“拆”而在于拆之前要想清楚三个问题拆什么、按什么拆、拆完怎么查。这三个问题没想明白就动手后面每个功能迭代都是在给自己挖坑。1. 单库单表为什么撑不住在聊怎么做分库分表之前我习惯先把“为什么要做”这件事拆透。很多团队一上来就急着选中间件、定分片策略其实连真正的瓶颈都没有定位清楚。单库单表到底是怎么一步步走向崩溃的我总结了四个阶段。1.1 存储容量的天花板这是最直接、最容易量化的瓶颈。MySQL 单表的数据量一旦过了千万级别B 树的层级会增加索引的维护成本会成倍上升。很多人听过“单表超过 2000 万行要拆”的说法但这个数字其实没有严格的数学依据它跟行长度、页大小、索引数量都有关。我见过 500 万行就慢得没法看的表也见过 8000 万行依然能跑得不错的表差异就在于行的宽度和查询的模式。真正致命的是硬件的物理上限。假设你的单行数据是 1KB单表 5000 万行就是 50GB 数据加上索引、临时文件、binlog、undo log实际占用往往是数据本身的 2 到 3 倍。哪怕你的 SSD 是 2TB也就够放三四个这样的表更别说备份、回放、扩容这种运维操作在这么大的数据集上有多痛苦。还有一个很多人忽略的点InnoDB 的缓冲池是有限的。热数据 10GB缓冲池也是 10GB勉强能用数据变成 100GB 的时候每次范围查询都可能把热数据挤出缓冲池然后磁盘 IO 飙升慢查询就来了。这就是我常说的“温水煮青蛙”——表面看是性能问题根子上是容量问题。1.2 连接数与并发瓶颈数据库的并发能力从来不是无限的。单库能同时处理的连接数是有上限的一般 MySQL 默认是 151。每个业务服务的连接池只要配置稍微激进一点比如一个服务配 50 个连接四个服务就能把单个数据库的连接数打满。连接打满之后的表现很典型新请求排队、超时、报 too many connections然后上游服务开始重试重试又带来更多连接最后整个数据库直接雪崩。这跟 Redis 连接数打爆的原理是一样的只是 MySQL 的每个连接开销更大崩起来更快。很多团队以为加机器就能解决那是想多了。单库的连接数上限是物理的你加再多的应用节点请求还是涌到同一个数据库上。这个时候就必须把一个库拆成多个库让连接请求分散到不同的实例上去。1.3 磁盘 IO 与主从延迟的恶性循环单库方案到了后期主从架构几乎是标配。但写请求集中在一个主库上读请求虽然分摊到了从库可主库的磁盘 IO 一旦饱和第一个遭殃的就是 binlog 同步。主从延迟会从几百毫秒一路涨到几秒甚至几十秒。主从延迟带来的问题很隐蔽应用刚写入一条订单立刻去从库查居然查不到。于是团队开始加“强制走主库”的逻辑把一部分读流量又引回主库主库压力进一步变大。为了缓解压力又加从库从库一多binlog 同步的网络开销也上来了延迟不降反升。这是一个死循环核心原因就是从根上没有把主库的写压力降下来。1.4 锁竞争与索引膨胀的隐性成本还有一个大家容易忽略的点就是锁竞争。单表数据量大、TPS 高的时候行锁、间隙锁甚至页锁的争用会变得非常明显。同一个商铺的库存记录、同一个账户的流水记录大量并发更新同一批热点行的时候InnoDB 的锁等待曲线会节节攀升。这个时候你去看 CPU往往只有 20% 的利用率但请求就是上不去卡在锁等待上。索引膨胀也是一个隐性杀手。单表数据量大了之后二级索引的体积可能比主键索引还要大。每次插入一行要维护的索引结构就更多写入放大加剧。更关键是查询优化器对索引的选择也会变的更纠结本来一条好好的 SQL数据量大了之后执行计划就变了。2. 拆分方案怎么选才不后悔理解了单库单表的瓶颈之后下一步是决定怎么拆。很多团队一听到分库分表就默认要上一个很重的中间件其实完全不一定。拆分的核心思路可以分两条路走垂直拆和水平拆。两条路的成本和收益完全不同选错方向后面会非常痛苦。2.1 垂直拆分先把业务切开垂直拆分说白了就是“一个大库按业务域拆成多个小库每个库只负责一类业务”。比如电商系统把用户、商品、订单、支付拆成四个独立数据库。这种拆法是对业务边界的一次重构其核心收益不是单表数据量变小而是把不同业务的相互影响隔离开来。垂直拆分在早期是非常划算的。第一它不需要改 SQL顶多调整数据源配置第二隔离效果很明显订单的慢查询再也不会拖垮商品浏览了第三还可以针对不同业务用不同的数据库规格用户库用普通 SSD 就够了订单库上高 IOPS 的云盘。很多中大型系统只做垂直拆分就能撑到很高的量级不一定非得上水平拆分。但垂直拆分有两个硬伤。一是它解决不了“某一类业务内部数据量太大”的问题比如订单库依然会有海量订单数据二是跨业务的查询会变成服务间调用以前一条 SQL join 就搞定的事情现在要调两次接口再做内存聚合。所以垂直拆分适合筑基不适合解决终局问题。2.2 水平拆分把同一张表切开水平拆分是对同一张表的数据行做切分把一张 1 亿行的表拆成 10 张表每张 1000 万行。这里又可以细分成三种组合方式只分表不分库、只分库不分表、分库又分表。只分表不分库就是把一张大表拆成多张小表但所有表还落在同一个实例上。这种方案解决了单表数据量过大的问题索引体积变小锁竞争变缓但是连接数和磁盘 IO 的瓶颈依然存在。只分库不分表就是把同一张表的数据按规则散落到不同实例上每台机器分到的数据量变小了连接数也分散了但每张表本身还是大表索引收益有限。分库又分表就是先把库拆开再把表拆小这是最彻底的方案也是运维复杂度最高的一种。我在实际操作中建议数据量在千万级、并发几百 QPS 的只分表就行数据量上亿、并发上千 QPS 的分库又分表才够。不要一上来就搞最重的方案架构设计最忌讳过度设计。2.3 拆分的判断标准三个硬指标很多团队问到底什么条件下应该拆分我从不凭感觉拍脑袋通用的判断标准是以下三个硬指标满足任意两个就要启动评估了。单表数据量超过 2000 万行或单表容量超过 20GB大字段多的表标准要更严格核心表的写入 TPS 持续超过 2000且慢查询比例开始上升通过优化 SQL 已经压不下来单个实例的存储余量低于 30%且预估未来半年的增长量会再次让容量告急。另外我还有一个个人经验可作为参考如果一张表的日新增数据量超过当时全表数据量的千分之一这张表就属于高速增长型必须提前规划拆分。很多公司是数据量涨到已经卡死才想起来拆那段时间的每一次代码发布都会很难受。3. 分片键与路由算法决定生死的关键分库分表最核心的设计不是中间件选型而是分片键sharding key的选取和路由算法的设计。分片键一旦定下来几乎就是不可逆的决定。为什么这么说因为分片键决定了数据怎么分布也就决定了查询怎么路由。3.1 分片键怎么选三个可选方向分片键的第一选择是主键比如用户 ID、订单 ID。用主键做分片的好处是天然唯一不需要额外生成且大多数查询都会带着主键条件。第二选择是业务自然键比如商户号、店铺 ID、设备 ID。这类键的特点是业务查询大概率都会带上。第三选择是时间字段适合日志、流水、事件类数据按天或按月分片。分片键选择有一条铁律必须覆盖 90% 以上的查询场景。比如订单表大多数查询都是“查某个用户的订单”那 user_id 就是天然的分片键如果订单表经常要按商家维度查那就需要用冗余或者映射表来解决。我见过最翻车的案例是在订单表上用 order_id 做分片键结果所有管理端查询都是按商家维度查每条 SQL 都要扫描全部分片比不分片的时候还慢。另外一个决策是单分片键还是复合分片键。我用过的方案里面单分片键是最常见的因为路由最简单。复合分片键虽然能更好地均衡数据和适配查询但计算复杂度和遗漏风险都高不少普通人我不太推荐一开始就用。3.2 四种主流路由算法对比路由算法决定了分片键值到底落到哪个库、哪张表。这个选择直接影响扩容的难度和数据分布的均匀度。我用一个表给大家讲清楚四种主流方案的优劣。算法核心逻辑优点缺点适用场景哈希取模分片号 hash(分片键) % N实现简单数据分布均匀集群扩容时几乎必须全量迁移大多数业务表数据量稳定范围路由按 ID 区间或时间区间划分扩容简单范围查询友好热点集中末尾分片数据量可能过大日志、流水、时序数据一致性哈希哈希环上虚拟节点映射扩容只影响部分数据迁移实现复杂度高有数据倾斜可能需要频繁扩缩容的弹性场景时间分片按天/月自动建表运维简单归档方便跨时间查询性能差订单、日志、监控数据我在大多数业务场景里首选哈希取模因为它的数据分布最均匀也最好理解。但如果你做的是时序数据时间分片或者范围路由会更符合自然语义。举个例子订单表按 user_id 哈希取模拆成 16 张表每个用户的所有订单都落在同一张表里用户维度的查询走一次路由就能定位效率非常高。3.3 容量规划的一个算例16 库 32 表够不够很多团队问分片数量到底设置多少合理这里我给一个实际算例。假设订单表单行大小 1KB计划支撑未来三年总订单量 1 亿行总容量就是 1 亿 × 1KB 100GB。不考虑冗余的情况下每张表数据量控制在 1000 万行以内比较安全偏保守的运维视角那需要的分片数是 1亿 ÷ 1000万 10 个分片。为了 2 的幂次扩容方便取 16 是合适的单表 1KB 行宽偏大如果行宽是 2KB那分片数翻倍到 32 更稳妥。从库维度看每个实例的磁盘建议只用到 50%-60%这样要给备份、日志、binlog 留空间。每库 8 张表16 个库就是 128 张表单表 1000 万行总容量 12.8 亿行留了 20% 余量够三年的增长。这个算例的逻辑是可以复用的先预估总量再定单表上限再反推分片数最后结合实例规格来确认库的数量。这里补充一个重要经验分片数的上限要预留未来三分之一的扩充空间同时保持 2 的 N 次幂就尽量不要定成奇数因为方便未来采用“翻倍扩容”方案代价会小很多。4. 分库分表的实操落地全流程分库分表的难点不在于写几行配置而在于整个迁移和切换的过程如何保证数据不丢、服务不停、风险可控。在我做过的多个实战项目中这个流程被反复打磨过基本稳定在六个阶段。下面把每一步的要点和逻辑讲透。4.1 阶段一存量数据评估与目标架构规划第一步不是写代码而是先做数据摸底。要统计每张核心表的数据量、行宽、增长速度、读写比例、慢查询 TOP 20以及跨表关联查询的清单。这些数据会直接决定拆分方案。我建议做一次全链路的依赖梳理哪些服务在读写这张表、哪些报表任务依赖这张表、哪些异步任务要扫描全表。比如你计划按仓库维度分片但有个定时任务每天要跨全部分片做统计那么就要优先把这个任务改成按分片并行执行。对于核心表我会归类为四类流量型强调写入吞吐、存量型强调存储容量、关联型强调查询路径、统计型强调聚合分析。每一种的拆分目标都不同这个分类是后续架构设计的主要依据。4.2 阶段二数据迁移工具的设计与实现从单表迁移到分库分表最怕的就是丢数据。业界最稳的方案是“全量 增量 校验”三段式。先做完一次全量快照迁移然后持续同步 binlog 增量最后做数据校验补齐差异。我详细说一下增量同步的实现要点。读取 binlog 可以用现成的中间件但要关注几个细节第一是位点启动增量同步时记录的 binlog 位点必须与全量快照的位点一致否则快照之后到启动增量之间产生的数据就丢了第二是幂等同步任务重启后要能根据唯一键跳过已经处理过的数据第三是延迟监控增量同步的延迟一般控制在 5 秒以内如果超过 30 秒就需要告警。全量迁移阶段可以采用分批拉取的方式按主键范围分批扫描每批 5000 行通过多线程并发写入目标库。迁移过程中把进度记录在元数据表里跑挂了可以从断点续传不用从头再来。这一步很关键不做断点续传大表迁移没法操作。4.3 阶段三双写与实时校验增量同步稳定跑起来之后进入双写阶段。所谓双写就是业务在写入老库的同时也写入新库。这里有三种常见的实现方式业务代码双写在 DAO 层做逻辑先写老库再写新库操作直观但侵入代码MQ 异步双写老库提交后发消息消费端写新库削峰效果好但存在消息丢失的可能中间件 binlog由同步组件完成双写业务无感知但整体架构偏重。我在实际操作中更喜欢组合方案核心交易链路用代码双写来保证实时性非核心场景用 MQ 异步来削峰。双写期间要做实时校验。校验的方式是做标记位对比——每天每个分片抽出一定比例的数据比对关键字段的哈希发现不一致就立刻回溯数据。校验的数据是“抽查 全量定期扫描”结合刚开始每天全量业务平稳后每周抽 10% 就够了。4.4 阶段四只读切流与灰度验证当校验一致率接近 100%就可以开始切流。切流策略我坚持两个原则先读后写先边缘后核心。先读后写的意思是先切部分读流量到新库通过线上真实的查询流量来做验证。如果只是代码层面没问题不代表性能没问题真实流量会暴露很多压力相关的隐患。我经常在灰度期发现新的分片数据分布不均或者某些分片的查询延迟比预期高不少。切流比例一般是 5% → 20% → 50% → 100%。每步都要观察四个核心指标查询 RT、写入成功率、慢查询数量、资源水位CPU、IO、连接数。这里有一个容易忽略的点一旦读流量切到新库如果新库和旧库之间还有增量同步链路在跑要特别关注同步延迟。如果延迟在涨说明新库的写入压力已经接近阈值这时候继续切流就是在给同步链路加负担非常容易出线上问题。4.5 阶段五全量切换与老库下线读流量切到 100% 之后还要再把写流量切完。写流量的切换更敏感我要求必须有 30 分钟内回滚的能力。回滚的核心是在老库继续保留一段时间的写入能力也就是代码里保留老库的数据源配置只是动态关闭。切换完成后先别急着下线老库而是再观察一段时间的增量同步延迟。确认稳定后停掉同步链路保留老库两个月作为备份。真正的老库下线和数据清理等到业务完全稳定、所有报表和数据分析任务都已切换到新数据源之后再操作。4.6 阶段六性能压测与容量验证最后一步是压在未来的。迁移完成后通常趁热打铁做一轮全链路压测验证目标容量内的性能水位。压测的方式很直白用生产流量回放或构造模拟流量对新架构做容量水位测试。在压测中要特别注意分片的“短板效应”。分库分表之后系统的吞吐量往往不是各个分片的平均值而是最差那个分片的瓶颈。比如 16 个分片里因为数据分布不均导致某个分片数据量是其他的 1.5 倍那这个分片的查询延迟就会拖累整体。压测的目标就是找出这个短板或者通过数据拆分重迁移来纠正分布或者在代码里加分片级熔断兜底。5. 分库分表后的经典问题与排查实录拆分完成不代表高枕无忧运维期才是问题真正浮出水面的时候。下面这些问题我几乎在每一个分库分表的项目里都碰到过按出现频率排序把排查思路和解决方案都给出来。5.1 分布式主键怎么生成才不踩坑单表拆成 128 张表之后数据库自增主键彻底失效了——因为每个分片各自自增必然撞车。用 UUID 不加处理也会有问题长度太长导致索引膨胀而且在 InnoDB 里 UUID 无序插入会导致页分裂、碎片化严重。我实践下来的推荐方案是 Snowflake 算法生成有序的分布式 ID。它的核心是把一个 64 位整数拆成“时间戳 机器号 自增序号”三段这样既能保证全局唯一又能保持单调递增在索引层面非常友好。团队自己实现痛点是时钟回拨——机器时间往回跳会导致 ID 重复需要加一个简单的时钟监测保护一旦发现时间回拨拒绝生成并告警。Snowflake 的 ID 还有一个妙用因为时间戳在高位同一个分片内的数据天然就是按时间序排列的这样很多“最近订单”“最新流水”的查询都能命中索引顺序扫描性能非常不错。作为对比纯粹随机的 UUID 做分片键会导致数据在各分片之间毫无规律冷热数据完全无法预判。另外一个更简单的方案是“分片内自增 全局唯一映射”为每个逻辑主键维护一个全局唯一的号段从 Redis 或数据库号段表里批量取号。这种方式适合业务对主键有序性要求极高的场景但成本比 Snowflake 高一些因为每次取号总有网络开销。5.2 跨分片查询和聚合怎么做分库分表之后最头疼的就是一条 SQL 里 join 两张分片规则完全不同的表。比如订单按 user_id 分片订单明细也按 user_id 分片那订单主表和明细表在同一个分片内 join 是丝滑的。但要是和商户表 join而商户表是按 merchant_id 分片的那就尴尬了——订单分片键和商户分片键对不上一条 join 就得扫描全部分片。解决办法我归纳为四种按成本从低到高排列冗余字段在订单表里冗余商户名称等低频变化字段查询时不再需要 join全局表把数据量小、被频繁 join 的表比如省份、字典、配置表在每个分片上各放一份这就是广播表的思路映射表维护一份“商户 vs 用户”的映射关系先查到分片键再路由到对应分片去查搜索引擎对于全文检索和复杂多条件组合查询干脆把数据同步到 ES让 ES 负责搜索聚合数据库负责事务性存储。聚合类报表查询也是同理。大促期间的实时大屏、每日销售汇总绝对不能直接跨分片去 count sum正确姿势是利用异步任务按分片并行聚合再把汇总结果写进结果表。这也就是 Lambda 架构里的 speed layer 思路——实时汇总一层、离线汇总一层业务层读合并结果。5.3 分布式事务与数据一致性取舍分库分表把原来本地事务跨成了分布式事务这是复杂性的大头。一单交易如果同时写了订单库、库存库、账户库任何一个库失败都会导致数据不一致。我在实战中给的方案非常务实能不用强一致就不用强一致业务设计上优先避免跨库事务。一个很奏效的方式是“先本地事务 后可靠消息”比如下单流程在订单库本地提交之后发一个 MQ 消息下游库存服务和账户服务消费消息来执行自己的本地事务。消息的可靠投递靠本地消息表保证业务数据和消息在同一本地事务里写入再由一个异步线程把消息状态标记为已发送消费端做幂等处理。如果确实需要强一致可以上 Seata 这类 AT/TCC 模式。AT 模式对业务侵入小靠全局事务管理器协调各个分支事务但是性能损耗不小不太适合高并发核心链路TCC 性能好一些但对业务的要求高需要你实现 Try / Confirm / Cancel 三个接口开发量大约多一倍。我的经验是宁可把业务流程重构为“最终一致”也别在一个事务里跨三个库那样做架构存量和故障爆炸半径都过大。5.4 分页排序与深度翻页的坑这是所有分库分表新手的噩梦。原来一句LIMIT 100000, 20就能搞定的事情分片之后变成了灾难每个分片都要查出前 100020 条然后在内存里做归并排序再取第 100000 到 100020 条。分片越多、页数越深开销越大性能会退化到不可接受。我常用的解法有几个。如果业务只是展示上一页和下一页用“游标方式”而不是页码把最后一个商品的 sort 值传到下一次查询作为 WHERE 条件每次只需查第一页之外的数据。大部分商品列表、订单列表都适合游标体验也不差。如果一定要跳页就把分页数据提前物化到缓存或 ES。比如把商品列表导入 ES页面上的跳页搜索走 ES商品详情再回数据库查。严格来说分库分表之后就不要指望“搜索引擎 数据库”这套逻辑在数据库里完整实现了必须引入额外的检索组件。5.5 数据倾斜最容易被低估的问题哈希取模理论上分布均匀但实际业务中数据倾斜几乎无法避免。最常见的场景是某些大客户的数据量远高于普通用户——一个头部用户的订单可能是普通用户的千倍即使按 user_id 取模落到某个分片上的数据量也可能远超其他分片。这会造成一个很尴尬的局面大多数分片很空闲一两个分片热得冒烟整体系统的容量被最热的分片限制住。针对这种问题我临时止血的办法是“热点区分片”检测到某个 user_id 的数据量超过阈值后把它单独拆分到独立分片再通过映射表记录特殊路由路径。长期方案是换分片键或者改用一致性哈希。一致性哈希在 hash 环上引入虚拟节点可以让热点键分布在更大的范围内数据倾斜的敏感度就会降低。但这也增加了路由复杂度需要结合业务稳定性来综合权衡——不是所有系统都有必要走到这一步。5.6 扩容的两种路径翻倍扩容与平滑扩容最后聊扩容因为这是分库分表最痛的运维操作也是“拆完就后悔”的高发区。经典取模方案最痛苦的点就在这里原来 16 个分片扩容到 32 个分片hash(user_id) % 16 变成了 % 32几乎所有数据都要重新分布。如果直接停服迁移那就是几个小时的业务中断如果在线迁移需要特别精细的双写和校验方案。我的建议是提前设计好“翻倍扩容”的路径保持分片数量为 2 的幂扩容时每个分片只拆分为两半。这样每个分片需要迁移的数据量就只有 1/2而且迁移逻辑非常规整——把原来各分片里 hash 结果落在“前半”的数据放新分片落在“后半”的留在原分片。这比全量重分布的迁移成本低一个数量级。另一种平滑扩容方案是“双写新算法 历史数据迁移”同时写旧路由和新路由读取时优先走新路由靠双写期间的数据迁移来拉平。这个方案和文章前面讲的数据迁移阶段几乎一样就是批量任务 binlog 增量 校验只是这次迁移的目标是新的分片数。我的经验是扩容前置到“数据迁移工具”阶段来做越早越顺手不要真的等到磁盘满报警那天才开始准备。最后补一句大实话如果你的系统未来可能从 16 个分片涨到 128 个分片刚开始就按 128 个分片来规划把大量杂表和数据全部打散到 128 个分片上。哪怕前期很多分片是空的也比后期做一次全量重分布舒服得多。写在最后的实战体会分库分表做了这么多年我最大的体会是这个架构模式从来不是“技术选型”而是“数据演进的必然结果”。单库能撑住的时候绝对不要拆但到了必须拆的临界点也不要犹豫不决犹豫的时间越长后面的迁移成本就越高。一个很实用的提醒分库分表的价值不只是“能存下更多数据”更是“隔离了故障域”。一个核心库出故障不至于拖垮整个应用。这就像你买房子不会只买一个大客厅而不分卧室数据库也一样存储上天生就应该是分而治之的。如果你现在还在设计新系统我的建议是先用单库单表舒服地开发业务但架构上把数据访问层抽象好预留未来做分片改造的空间。比如 DAO 层尽量不要把 SQL 散落在业务代码里统一走一个数据访问入口。这样未来要加分库分表的时候改动面就控制在一个模块内不需要翻遍整个项目改 SQL这是成本最低的前置准备。最后再分享一个小经验拆分前一定要多花一周的时间把数据迁移和回滚的剧本演练几遍。这个投入看起来“不产出功能”但在真正出故障的那天它就是救命的稻草。我见过太多的团队把精力全花在分片算法和中间件配置上结果真实迁移的时候才发现在线变更方案根本不成熟最后只能加班救火。咱们做技术的最值钱的能力不是怎么写代码而是怎么把风险提前想清楚、把预案做扎实。