
分库分表这个系列写到第二篇说明第一篇那些概念性的东西你已经消化得差不多了什么垂直拆分、水平拆分、范围分片、哈希分片心里大概都有数了。但概念懂了和真能落地是两回事。我在生产环境里完整经历过一次分库分表改造从评估、设计、迁移到上线后的各种“惊喜”整个过程走下来最深的感受是网上讲分库分表原理的文章一抓一大把但真正能告诉你会踩哪些坑、哪些环节会卡你一周的文章不多。这篇就聊实战聚焦在“你决定要做分库分表之后怎么把事情干完、干稳”。包含四个部分整体架构和分片策略怎么定、存量数据和增量流量怎么平滑切换、分布式事务和ID生成怎么选型、以及上线之后那些高频问题怎么排查。内容会比较干建议收藏之后对着自己的项目逐步对照。1. 整体架构设计与分片策略进阶1.1 分片策略选择别只看一张表第一篇文章聊过分片策略大类上就是范围分片和哈希分片。但在真实项目里很少会只用一种策略大部分系统最终是混合的。范围分片的好处是天生支持范围查询按时间分片尤其适合流水类数据比如订单、日志、流水。但致命弱点是热点问题——你按月份分片当月的数据全打在当月库上月初那几天某个库的QPS就是其他库的好几倍。如果你用哈希分片数据分布均匀了但范围查询基本废了每次都要打到所有分片上去聚合。我当时的做法是“时间范围 业务维度哈希”的二级路由。举个例子订单表按照user_id做哈希分片同时物理表名带有月份后缀。也就是说先按用户哈希定位到某个库再按订单归属月份定位到某张表。这样既保住了用户维度的数据聚集性又能在冷热数据上做文章——半年前的表直接归档不需要动线上分片逻辑。另外分片键的选择是真正决定生死的一步。我见过不少团队前期拍脑袋选了订单ID做分片结果后面业务方要求按商家维度查订单列表每次查询都是全分片扫一遍哪个库都躲不掉。分片键的黄金法则是选那个你最核心、最高频、最无法妥协的查询维度。如果核心查询有多个维度优先保证一个其他的用冗余表或者索引表去补而不是试图让分片兼容所有查询。所以第一步不是讨论分几个库、几张表而是先把核心查询路径画出来列清楚哪些查询必须在单分片上命中哪些能接受跨分片聚合哪些查询可以适当牺牲实时性。这个排序做不完后面全是返工。1.2 基因法分片一个被低估的设计分片键只选一个但业务上又确实需要从另一个维度也能定位到同一个分片这时候基因法是很实用的设计但很多人没意识到这个思路的价值。核心原理很简单假设你按user_id哈希分成 16 个库也就是 4 bit2^4 16。那么在生成订单号的时候把user_id的低 4 bit 拼接到订单号里去。这样以后拿到一个订单号直接看它的低 4 位就能知道它落在哪个库根本不需要反查用户ID也不需要维护映射关系。这个做法成本极低收益却非常明确。比如客服系统或售后系统用户只会报给你“订单号是多少”你拿到订单号就能直接路由到对应分片不需要先查一张全局映射表或者缓存。我的经验是凡是需要在多个业务场景里按不同维度找数据的能往主键里埋基因尽量埋。需要注意一点基因位数的计算要跟分片数量严格挂钩。如果你未来要扩到 32 个库那基因位至少要 5 bit。如果你只留 4 bit 后面扩到 32 库路由逻辑就全乱了。所以别光图省事先把未来的容量规划想清楚再定基因位的长度。容量规划这件事后面第四部分还会专门细说因为它直接影响到你现在的分片设计。1.3 改造节奏存量迁移不是最难的很多人以为分库分表改造最难的是数据迁移其实对我来说最难的是推动业务方配合改造。分库分表一旦启动所有 SQL 都得过一遍能改的改不能改的要么放弃功能要么换实现方式。这个工作量不在 DBA也不在一两个后端而是整个研发团队。我建议节奏分四步走先做 SQL 梳理和静态检查把所有涉及目标表的 SQL 全部拉出来逐个判断是不是带上了分片键。做架构改造和双写代码新代码先上线只双写旧库旧表不读新库跑一到两周验证数据一致性。做数据校验和灰度切读先切 5% 流量到新库新表观察性能和数据正确性逐步扩大到 100%。最后下线旧库旧表这个动作反而是最简单的备份完毕直接撤。这个节奏里最容易出问题的其实是第二步的双写。双写如果写失败了怎么办是阻塞主流程还是异步补偿两边数据不一致以哪个为准这些问题没有提前设计好上线那天就是灾难现场。我见过一个团队双写只做了同步写中间新库写入失败直接抛异常导致主链路一起挂掉。双写必须考虑失败降级新库写入失败不能影响主链路只记录日志等补偿任务去处理。2. 存量迁移 增量同步的实战方案2.1 迁移架构存量与增量两条线并行数据迁移是分库分表改造里最让人紧张的部分因为数据量在那摆着几百G上T都是常事。全量拷贝不难难的是你拷贝完之后新数据还在继续写怎么保证两者不冲突、不丢数据。标准做法是“存量 增量”双线并行。存量线负责把旧库已有的历史数据按分片规则写入新库增量线负责持续读取旧库的 binlog把新发生的变更实时同步到新库。两条线同时跑跑完之后在某个时间点停旧库写入口再把增量追平切换完成。存量迁移的写入顺序要按分片来组织不要按原库顺序读一批写一批。按原库顺序会导致新库的分片写入压力完全不可控——可能连续几批数据都落在一个分片上把那个库打满其他库闲得发慌。正确做法是读出来之后先做分组缓冲每个分片一个队列各自批量写入控制好并发和批次大小。增量同步这块用 Canal 或者 DataX 这类现成工具是合理的但别指望工具能解决一切问题。binlog 同步最大的坑在于你如果是从库拉 binlog需要注意从库本身的延迟如果你直接在业务代码里塞了又一个 MQ把所有写操作都发一份出去那就要考虑消息丢失和重复消费的问题。重复方面至少要做到幂等否则丢一个消息可能就是一笔订单对不上账。2.2 数据校验不能靠抽查迁移完不等于迁移完校验不到位后面出了问题定位成本极高甚至不知道数据坏在哪个环节。我的习惯是迁移过程中至少做三轮校验第一轮全量校验数量。新库每个分片的记录总数必须跟旧库按分片规则重新计算的期望值一致。这条只能说明数量对得上不能说明内容对。第二轮抽样校验内容。按业务主键和关键字段把新库数据跟旧库比对。抽样比例可以动态调数据量小的表全量比对数据量大且字段多的表至少保证每个分片抽到足够样本。第三轮业务校验。跑几个关键的业务查询拿真实用户ID去路由查询结果跟旧库对比。这个环节最能发现问题因为代码层面的字段映射、类型转换错误只有业务查询才会暴露。校验阶段最容易被忽略的是唯一键和索引。新库建表时如果没把唯一键建对双写期间就可能写进重复数据而校验脚本查数量是查不出来的。我吃过这个亏当时查了一轮数量没问题结果上线后用户反馈重复订单查下来才发现新库唯一键少了两个字段。2.3 灰度切换留好回退开关切流量这件事很多人以为就是改个配置把读流量指向新库就完了。实际上灰度切读要考虑的问题不少。第一切读之前要保证新库的读性能真的能扛住。别光看单条 SQL 的耗时要压测尤其要测带着分片键的 SQL 和没带分片键的 SQL 混在一起时的表现。分片之后原本单库的慢查询可能被放大到多库任何一个分片出现瓶颈都会拖慢整体请求。第二灰度切读必须带开关。我当时是用了配置中心的开关按用户ID哈希的范围做切流从 5% 到 10% 再到 50%每一步观察至少一天包括数据正确性、接口耗时、错误率、数据库连接池使用情况。不要走“切了就回不来”的路开关必须先设计好。第三回退预案要完整。如果切到 60% 流量后发现严重问题怎么快速回退到旧库回退期间产生的增量数据怎么处理这些都要提前写成文档哪怕最后没用上也必须写。真出问题的时候团队成员手忙脚乱有一份清晰的操作手册能挽回不少损失。3. 分布式事务与分布式ID的落地取舍3.1 分布式事务能不用就不用分库分表之后原来单库本地事务能搞定的跨表操作现在可能变成跨库跨服务的操作。分布式事务的话题就绕不开了。我的观点很明确分布式事务能不用就不用尤其不要一上来就推 Seata 这种重量级方案。不是因为技术不好而是分布式事务的成本不在于框架本身在于它改变了业务代码的写法要求所有资源都必须纳入全局事务管理隔离级别和锁的范围都会变化排查问题的复杂度直接上一个数量级。我实际的取舍标准是看业务能不能接受最终一致性。大部分互联网业务场景都能接受最终一致比如下单后扣库存、发积分、写流水这些操作之间没有强一致的必要只要最终对得上就行。真正确实需要强一致的场景也有比如金融支付核心链路但这种场景往往也不适合直接套分布式事务框架更可靠的路径是流程编排 本地消息表 幂等重放。说到这套最终一致性方案核心思路是这样的本地事务写业务数据 写消息表放在同一个事务里保证业务数据和消息要么都成功要么都失败。然后一个异步任务扫描消息表把消息可靠地发送到 MQ。消费方拉取消息执行下游操作操作成功才确认消费失败就重试同时保证消费逻辑幂等。这个方案的实现成本很低但能覆盖绝大多数跨服务数据最终一致的场景。花钱买强一致的事务框架不如先把业务链路里哪些地方需要幂等、哪些地方需要重试、哪些地方需要补偿梳理清楚。梳理清楚之后你会发现需要强一致的地方可能比你想的少得多。3.2 分布式ID选型雪花算法与它的变体分库分表之后数据库自增主键就没法用了分布式ID生成就成了刚需。选型上基本都是雪花算法的路线但雪花算法不是拿来就完事有几个细节必须注意。经典雪花算法是 1 bit 符号位 41 bit 时间戳 10 bit 机器ID 12 bit 序列号。41 bit 时间戳能用到 69 年10 bit 机器ID能支持 1024 个节点12 bit 序列号代表每个节点每毫秒能生成 4096 个ID这个规模大部分业务都够用。但问题是ID是纯数字递增的有些场景下暴露了业务量而且时钟回拨会导致ID重复这个在分布式环境下确实可能发生。我见过两类变体值得提一下。一类是给雪花ID加入业务含义比如前面提到的基因法把分片基因拼进ID里这本质上就是对分布式ID的一种定制。另一类是改造机器ID的分配方式不用配置而是动态注册减少运维成本。但核心都跑不出“时间戳 机器标识 序号”这个框架。实际生成ID的方式我推荐用独立的ID生成服务而不是每个应用各自生成。原因很简单独立服务方便做统一监控和预警也方便切换算法。但要注意这个服务本身的高可用不能用依赖数据库的方式做否则ID服务挂了会直接卡住全链路。3.3 分布式ID选型对照选型的时候很多人纠结用哪种方案我是从运维成本和后续扩展性两个维度来考虑的。这里给一个对比视角方案优势劣势适用场景雪花算法纯本地生成依赖少性能高时钟回拨需要处理ID可预测大部分通用业务带业务位的雪花变体路由方便支持基因分片实现稍复杂位段需要规划需要按业务维度路由的场景号段模式按段批量取号节省DB压力依赖数据库扩容需要规划中小规模ID需求架构简单UUID生成简单全球唯一无序、易造成页分裂存储效率低非索引字段、离线场景这个表不一定要完全照搬但选择逻辑可以参考。优先想清楚ID要不要承载路由信息业务是否需要按ID做趋势递增排序比如某些业务按ID倒序展示ID服务挂了能不能接受降级这三个问题聊透了方案八九不离十就出来了。4. 运行期高频问题排查与容量规划4.1 分页与排序深分页别硬扛分库分表之后ORDER BY ... LIMIT 100000, 20这种深分页查询是最让人头疼的。单库时代数据库可以直接定位到偏移量然后取数据分片之后就不行了你得先从每个分片把第 100000 到 100020 的数据都捞出来然后在应用层做内存排序和再分页。数据量一大这个操作的耗时和内存消耗完全是灾难。解决思路有三个方向。第一个是禁掉深分页只允许通过上一页的最后一条记录ID来做游标分页也就是WHERE id ? ORDER BY id DESC LIMIT 20这种每次只查一页响应快。第二个是加缓存把热数据的分页结果提前算好放缓存里。第三个是如果确实需要任意翻页那就接受牺牲性能用异步预聚合或者 ES 等搜索引擎来兜底而不是直接打在数据库上。我自己是直接禁了深分页产品接受不了再说但上线至今没有用户真的需要翻到第 5000 页去。很多所谓的技术难题实际上可以通过调整产品交互来解决。4.2 跨分片聚合与全局表分片键从来不是万能的总有查询会漏掉分片键跑起来就是全分片扫描。尤其在多表 join 的场景里分库分表之后 join 已经不能指望数据库帮你完成了。拿最典型的用户表和订单表来说如果订单表按用户分片用户表又按别的维度分片两张表根本对不上join 无从谈起。通用套路是全局表。把用户、商品、类目这些数据量不大、更新频率低、又经常被关联查询的表在每个分片上都放一份全量数据。这样 join 操作就变成了单分片内部的 join性能完全可控。缺点也明显全局表的更新需要广播到所有分片所以只适合低频更新的数据。另一个做法是数据冗余在业务写入时就把需要通过另一维度查询的数据冗余过来。比如订单表按用户分片但你还需要按店铺查订单那就同步建一张店铺维度的订单索引表把订单ID和店铺ID的对应关系冗余进去。写入时多写一份查询时先查索引表定位分片再回查订单表。说白了用空间换时间用写放大换读性能。4.3 热点分片与容量再规划哈希分片理论上能把数据均匀打散但真实业务总有热点。比如某个头部用户产生的数据量是普通用户的几千倍如果这个用户恰好哈希落在一个分片上那个分片就会被打满其他分片负载很轻。这类问题光靠哈希解决不了得做二次拆分把热点用户的ID再次做内部哈希分散到更多子表。判断热点不能拍脑袋要上监控。每个分片的连接数、QPS、磁盘增长、慢查询数这些指标必须每天看。我遇到过一个情况是某个分片磁盘快满了但其他分片剩余空间还有一半查下来就是某个大客户的数据增长太快。这种情况趁着业务压力不大尽早做数据均衡不要拖到磁盘快满了才开始迁移。容量规划是分库分表设计阶段最容易被低估的事情。很多人只看了当前数据量拍脑袋分了 8 个库或者 16 张表结果不到半年就要再扩容。容量规划的核心是留够冗余数据增长要按三年的复合增长率估算连接数要按峰值QPS来核算存储要有 50% 以上的余量分片数量最好是 2 的 N 次方因为哈希取模原理决定了按 2 的幂次扩容时迁移成本最低只需要迁移半数数据。我对读者的建议是如果你现在库表还没分先把三年后的数据量、QPS、存储空间这些数字大致估准再回头来定分片键和分片数。分片数宁多勿少因为从 16 片扩到 32 片和从 8 片扩到 16 片工作量不同但都要动数据的。一次规划到位后面能省非常多的事。还有一个容易忽略的细节新库建表的时候就把归档方案想好。分库分表不是为了把数据永远堆在热库里而是要让你有能力把冷数据扫到归档存储。我见过不少系统分完库分完表数据过了两年照样堆在热库分库分表只是把一个大的冷库变成了好几个大的冷库本质上没解决问题。最后说点实在的分库分表做完整轮改造之后我个人最大的感受是分库分表不是数据库问题的银弹它是你数据量大到单库实在扛不住时的必然选择但它会引入新的复杂度这些复杂度必须靠规范、监控、工具链来对冲。不要以为拆分完就一劳永逸了恰恰相反拆分完才是运维和排查工作的开始。如果你正准备启动这个改造我的建议是先想清楚分片键和容量规划再开始动手。把设计和规划做扎实比什么技巧都重要。另外灰度开关和数据校验脚本一定要提前准备这是整个迁移过程的安全网。最后一个小技巧在双写期间每天跑一次对账任务把两边不一致的数据列表输出出来哪怕只有几条也要看很多时候问题就藏在那几条里早发现远比晚发现好处理。