
1. 开篇分库分表系列第三篇到底聊什么分库分表这件事我前后写过两篇第一篇聊的是“到底什么时候该分”第二篇聊的是“垂直拆分和水平拆分怎么选”。不少读者在评论区催更问我能不能把“真上生产”踩过的坑整理出来。这篇第三篇我就把它当实战复盘来写。分库分表不是加几个中间件、写几条SQL就完事真正的难点往往在分片键选择、数据迁移、扩容、框架配置、跨库查询这些环节里任何一个环节没想清楚后面都要用加班来还。这篇主要适合两类人一类是数据库已经明显吃紧正准备做分库分表改造的团队另一类是已经分了库、分了表但经常被扩容、迁移、分布式事务折磨的开发者。我会把从选型到落地、从迁移到验证、从框架配置到问题排查的完整路径都过一遍尽量用真实场景说话。能让你看完之后对自己项目里该不该分、怎么分、分了之后怎么维护有更具体的判断。1.1 先回顾一下前两篇的结论前两篇的核心观点是分库分表是手段不是目的。单库连接数不够了先考虑缓存、读写分离、归档单表数据量到了几千万甚至上亿、写性能明显下降再考虑水平拆分业务模块之间耦合太深、各自表结构差异巨大才有垂直拆库的必要。我见过不少团队表才三五百万行就急着分库分表结果引入分布式事务、复杂查询被限制反而把简单业务搞复杂了。所以第三篇的所有内容都是建立在“你确实有必要做分库分表”这个前提上的。2. 分片键选型与分片策略这一步错后面全是泪谈到分库分表很多人第一反应是选框架、写配置但实际上最核心的决策是“按什么字段来分片”。这个字段选错了后面所有的路由、查询、扩容都会跟着错。我见过一个项目订单表按创建时间做了按月分表结果每个月月底的最后几天单表数据量暴涨写入热点集中在月底其他日期几乎没压力这等于砌了墙但墙里埋了雷。2.1 分片键选型的几个硬性标准分片键必须具备几个条件缺一个都不建议直接用。第一是查询维度要匹配。分片键必须是业务中最高频的查询条件。比如电商订单表买家查“我的订单”是最高频的那么按买家ID分片就是一个合理选择如果按订单ID分片买家查订单时就得路由到所有分片性能直接退化。第二是数据分布要均匀。理想情况下每个分片的数据量、访问量都差不多不能出现某个分片特别“热”。比如按地区分片核心城市的业务量远超其他地区就会造成数据倾斜。第三是分片键的值要稳定。业务一经产生这个字段就不能变否则迁移成本极高。我见过一个系统用手机号作为分片键结果用户换号之后历史订单全都查不到了因为路由规则变了数据找不回来。这类问题排查起来非常痛苦。所以选分片键之前一定要把核心查询SQL全部列出来统计哪个字段出现在WHERE条件里的频率最高再结合字段本身的分布情况做决定。站在我个人的角度宁可牺牲一点“平均分布”的完美性也要优先保证“高频查询能单分片命中”。2.2 常见分片策略对比别只会取模分片策略直接决定数据怎么落库。目前主流的有这几种| 策略 | 实现方式 | 优点 | 缺点 | | 取模 | 分片键哈希后对分片数取余 | 实现简单分布均匀 | 扩容时数据迁移量大重新哈希比例高 | | 范围分片 | 按主键或时间区间分片 | 扩容简单便于归档 | 容易产生热点分片 | | 一致性哈希 | 哈希环 虚拟节点 | 扩容时迁移数据少 | 路由逻辑复杂运维成本略高 | | 时间分片 | 按年/月/日分表 | 归档方便适合流水类 | 业务高峰时段单表压力集中 |取模是最常见的入门方案但不代表它适合所有人。假设你有8个分片取模8之后业务增长要扩容到16个分片那么原来落在分片0的很多数据因为模数变了有一部分要搬到分片8几乎一半数据都需要重分布。这个过程如果不做在线迁移就意味着停机。我个人的建议是如果数据量相对可控、增长不那么凶险业务系统又追求简单取模没问题。但如果确定未来两三年内会有明显的扩容需求最好一开始就考虑一致性哈希。一致性哈希通过哈希环上的虚拟节点把数据映射到物理分片扩容时只需要把一部分虚拟节点从旧分片挪到新分片迁移的数据量远小于取模重新哈希。2.3 一个订单场景的完整推导拿电商订单数据来说假设订单量已经突破3000万需要拆分成16个库、每库64张表。我比较推荐的分片键是买家ID。判定依据是买家后台、“我的订单”页面、售后列表这些都是买家维度按买家ID分片后一次查询只打一个分片速度可观。同时买家ID本身是数字整数哈希后的分布相对均匀不会出现某个分片数据量特别大。但也有特殊情况。比如运营后台要按商家维度查订单这就涉及跨分片扫描。解决方案有几种一是按商家ID再建一份只读的汇总索引表利用异步任务同步二是引入搜索引擎把订单宽表导入Elasticsearch供后台多维查询三是在查询网关层做分片并发聚合返回结果再合并。这三种方案各有利弊我在第五部分会展开说。关键点是没有任何一个分片键能满足所有查询选一个主分片键其余的通过旁路数据或关联表来兜底。3. 扩容与数据迁移最容易被低估的环节真正开始动数据迁移时很多人才发现自己对分库分表的理解还停留在纸面上。我这里说的扩容既包括分片数从少变多也包括从一个数据库迁移到另一个数据库或者从单库迁移到分库分表结构本质都一样数据要重新分布而且多数时候业务不能长时间停。3.1 停机扩容到底可不可行先聊一个比较“古老”但也最简单粗暴的方案停机扩容。流程不难理解提前选定一个业务低峰期发布一个维护公告停掉所有写入然后跑数据迁移脚本把旧库按新的分片规则写入各分片迁移完成后做数据校验接着切换应用的数据源配置最后恢复对外服务。这个方案最大的好处是简单只要迁移脚本没问题风险相对可控。坏处也明显停机时间等于数据迁移时间数据量越大停机越久。对C端业务来说你很难让大量用户等两小时。所以我的建议是能用在线迁移尽量用在线迁移哪怕复杂度高一些。停机扩容只适合允许低延迟、小规模数据或者内部系统的场景。3.2 双写迁移方案最稳妥的在线迁移思路在线迁移我做过最稳的一套流程可以用四个字概括全量 增量 双写 校验。这里的双写指的是迁移期间把新写入的数据同时落到旧存储和新分片和分库分表本身不是一回事。常见做法如下先做一次全量快照把旧库的数据按新的分片规则导入新分片。这个阶段不需要停服只需要尽量避开高峰。开启增量同步。可以通过监听数据库binlog也可以把业务里的写操作同时发送到消息队列再由异步任务落新分片。改造业务应用写入时同时写旧库和新分片保证从某一时刻开始新产生的所有数据两边都有。在双写持续一段时间后对旧库和新分片做一致性校验。发现差异数据用增量任务补齐。当差异越来越小达到可接受的范围后可以先把读流量切到新分片观察一段时间。确认稳定后再停掉双写彻底下线旧库。这里面最容易出问题的是增量同步的顺序和幂等。因为旧库写入和新库写入不是同一个事务通道异常时会造成数据丢失或重复。所以一定要在同步任务里加入幂等机制比如按业务主键判断同一主键只允许一条数据落库。这个坑我踩过好几次最严重的一次是漏了几万条订单数据第二天报表对不上花了整整一天才排查清楚。3.3 一致性校验怎么做才靠谱很多人迁移完就上了完全不校验我觉得这是在赌运气。哪怕迁移脚本写得再小心也免不了边界问题。一致性校验有几个层次。第一层是数量校验统计旧库各分片数据量与目标分片数据量是否一致。第二层是抽样校验从每个分片取一定比例的记录比较关键字段是否一致。第三层是范围校验针对时间、状态这类字段分别做聚合统计对比两边数据是否吻合。第四层是逐行比对如果数据量能接受可以全量拉取主键和校验码比较新旧两份数据的哈希值。校验工具可以自己写也可以借鉴一些开源思路但基本上都是围绕“数据ID清单 字段指纹”来做的。我常用的方式是从旧库查出所有主键按分片规则算出每条记录应该落在哪个新分片再在对应的新分片里按主键取数据比对关键字段。这样能快速定位具体哪些主键的数据缺失或错误。这个脚本不复杂但很值得多花时间打磨因为每次迁移都能复用。3.4 回滚预案是最后的底牌在线迁移做得再好也要有回滚预案。我说的回滚不是指“出了问题把数据全部倒回去”而是保留旧库的只读权限和一段时间内的binlog日志。一旦新分片出现严重的数据完整性问题应用可以马上切回旧库同时暂停增量同步任务避免新数据继续污染。这里有一个细节双写期间旧库不要停也不要轻易清理归档。很多人觉得新库都写好了旧库留着没用结果出了问题才发现旧数据已经不完整想回滚都回不了。按照我的实践经验双写停止后旧库至少要保留7到30天具体看业务对数据丢失的容忍度。4. 框架选型与配置ShardingSphere落地实操分库分表的落地离不开框架目前国内比较主流的有ShardingSphere、MyCat。我自己用得比较多的是ShardingSphere-JDBC因为它内嵌在应用里不需要单独的中间件集群对既有架构侵入最小。下面我以它为例讲几个核心配置和使用要点。4.1 当前主流框架的差异怎么选不纠结先把结论放前面如果团队规模不大想尽量少维护一套中间件集群优先选ShardingSphere-JDBC。如果公司有专业DBA希望多个应用共用一套数据中间件或者你的开发语言不是Java再考虑ShardingSphere-Proxy或MyCat。ShardingSphere-JDBC以jar包形式嵌入应用走的是应用直连数据库SQL解析、路由、改写、归并在应用内完成。好处是性能损耗小、部署简单坏处是每个应用都需要重复配置而且如果应用本身不满足Java环境就没办法用。ShardingSphere-Proxy则是一个独立服务应用通过MySQL协议连接它由它转发到后端数据库类似于一个数据库网关。好处是对应用透明连接客户端像连接一个MySQL一样坏处是网络链路上多了一层服务高并发时需要考虑Proxy自身的瓶颈。MyCat是历史比较久的一款中间件产品国内很多老旧系统还在用。它的路由能力不错但功能迭代相对慢对复杂SQL和分布式事务的支持不如ShardingSphere灵活。我个人不太建议新项目从MyCat起步除非团队里有同学对MyCat非常熟悉否则后面很多边界问题都得自己踩。4.2 分片规则配置示例一个YAML讲清楚用ShardingSphere-JDBC配置分片关键是把数据源、分片算法、表路由规则列清楚。下面我写一个简单的订单分表示例总共两个数据源ds0、ds1每个库里有两张订单表t_order_0、t_order_1按照订单ID取模路由。dataSources: ds0: url: jdbc:mysql://192.168.1.10:3306/db_order_0 username: root password: change_me ds1: url: jdbc:mysql://192.168.1.11:3306/db_order_1 username: root password: change_me rules: sharding: tables: t_order: actualDataNodes: ds${0..1}.t_order_${0..1} databaseStrategy: standard: shardingColumn: order_id shardingAlgorithmName: db_hash tableStrategy: standard: shardingColumn: order_id shardingAlgorithmName: table_hash shardingAlgorithms: db_hash: type: HASH_MOD props: sharding-count: 2 table_hash: type: HASH_MOD props: sharding-count: 2这里最需要用点心的是actualDataNodes。它把物理的表节点都列出来格式是“数据源名 表名”的组合。上面配置的含义是两个数据源分别有t_order_0和t_order_1共4个物理表。order_id经过HASH_MOD算法取模先算出应该落到哪个库再算出落到哪张表。取模算法非常简单就是对分片键算一次哈希然后对分片数取余。如果你的业务要求数据更整齐也可以自己实现一个算法类写上自己的路由逻辑。配置好了之后应用里怎么写DAO呢其实和普通MyBatis差不多。只要把表名写成逻辑表t_order框架会自动改写SQL。比如SELECT * FROM t_order WHERE order_id ?框架会解析出order_id的值根据配置好的分片算法把SQL改写为形如SELECT * FROM t_order_0 WHERE order_id ?这样的语句并路由到对应数据源执行。这里有个容易踩的坑如果查询条件里没有分片键order_id框架会做全分片路由也就是把SQL广播到所有分片节点然后汇总结果。数据量大的时候这个操作非常重甚至会把数据库拖垮。4.3 绑定表与广播表解决JOIN和字典表的两个关键分库分表之后JOIN查询是最让人头疼的。如果两张表都按同一个维度分片而且分片数量一致那么它们就是绑定表可以允许JOINShardingSphere就能在同一个分片内执行JOIN避免跨库组装。比如订单表t_order和订单明细表t_order_item都按order_id取模配置上绑定表关系后框架在路由时会保证两张表的对应分片落在同一个连接内。配置绑定表很简单在表规则下面增加一段bindingTables: - t_order,t_order_item有了这个配置框架就知道t_order和t_order_item是一组的JOIN时只会在分片内部完成不需要跨库传输数据。这个设置对性能影响非常明显有客户环境里加了绑定表配置之后原本因为跨库JOIN导致的慢查询直接降了一个数量级。另一类是广播表适合那种每个分片都要有副本的字典表比如订单状态码、商品类别这类很少变动的数据。把所有数据放到每个分片SQL查询时就不用跨库去找字典表了。配置上同样很简单在sharding规则里增加broadcastTables: - t_dict_status更新广播表时框架会同步更新所有分片里的副本所以这类表只适合低频更新的数据。如果写入频繁广播表的同步压力会很大这种场景还是建议放到缓存里。5. 分库分表后的四大痛点与排查实践分库分表搞完了不代表一劳永逸反而会出现一些新问题。分布式ID、分布式事务、跨库分页、统计聚合这四个是我被问到最多、也在真实系统里踩得最深的方向。5.1 分布式ID别再用数据库自增ID硬扛全局唯一单表里的自增ID到了分库分表之后最直接的冲突就是不同库、不同表的ID会重复。比如订单ID在库1里是1、2、3在库2里也是1、2、3一旦数据需要跨库聚合ID就当不了全局主键了。所以分布式ID是分库分表之前就要想清楚的问题。目前比较主流的是雪花算法和号段模式。雪花算法生成一个64位的整数由时间戳、机器ID、序列号组成优点是无中心、生成速度快、趋势递增缺点是时钟回拨可能导致ID重复要小心处理。号段模式则是从一张专门的ID表里批量取号比如每次取1000个号段应用再本地分配优点是ID连续、性能可控缺点是表本身可能成为瓶颈。我的建议是如果没有特别强的ID可读性和连续需求用雪花算法系列方案就够了。如果业务上需要可读性强的订单号可以考虑在雪花ID基础上拼上业务前缀或者干脆用Redis 日期 自增序列来生成但一定要保证序列的并发能力。注意不要在分库分表之后再去补分布式ID因为大量存量数据需要重新生成ID迁移成本极高。5.2 分布式事务能靠业务规避就不要硬上分库分表后一个业务操作很可能要写多个分片原来依赖单库事务的原子性就不成立了。比如下单时需要同时写订单表和库存表如果这两张表落到了不同的库就构成分布式事务。做分布式事务的方案无非是XA两阶段提交、TCC、本地消息表、事务消息等。我真实的建议是不要为了架构完美就上复杂事务框架能用业务规避就尽量规避。比如库存扣减可以设计成在同一个分片内执行“订单状态更新 库存扣减”的SQL如果确实无法避免跨分片再考虑事务消息或TCC。TCC的复杂度比较高需要业务提供Try、Confirm、Cancel三个方法对团队要求很高本地消息表则比较依赖消息可靠性和消费幂等设计。这里要特别提醒一句分库分表框架本身提供的事务能力比如ShardingSphere里的分布式事务底层还是依赖各种事务组件不是所有流量都适合强制走强一致性。很多时候“最终一致性 对账任务”比硬上强事务更能落得下去。5.3 跨库JOIN、分页与排序这是最大的性能杀手分库分表之后SQL的包容度会直线下降。你原来写一条JOIN现在框架可能改写得很费劲你原来写一条LIMIT 10 OFFSET 100000现在每个分片都要各自查100010条再汇总后排序取前10条这种“深分页”会让数据库不堪重负。先讲JOIN。分库分表之前能拆的JOIN尽量拆成多次查询在应用层做数据组装接着考虑用绑定表设计保证同维度表可以在同一分片内JOIN。如果两者都不满足那最好把JOIN查询的维度做成冗余数据比如在订单表里冗余商家ID查询商家订单时直接走订单表自己的字段不再join商家表。再说分页。除非业务明确需要随机访问大页码否则建议使用“向下翻页”替代“跳页”。也就是说前端每次只传上一页最后的ID或时间戳后端在SQL里用WHERE order_id 上一页最大值 ORDER BY order_id LIMIT 20来查询这样每个分片都能快速定位不必全部查出来再聚合。对绝大多数分页场景来说这已经够用了。如果后台系统必须支持任意条件组合、模糊查询、复杂聚合那就不建议在分出来的数据库上死磕。我会在分库分表之上引入一套独立的查询引擎用异步同步的方式把各分片数据汇总到搜索引擎或宽表里。对这确实多了一套系统但是它能解决路由SQL解决不了的问题值得投入。5.4 统计聚合实时性要求不高就别直接怼数据库订单查出来需要做统计这是分库分表场景里最常见的需求。比如看板要统计今日订单量、销售金额如果每次统计都全分片扫描数据库压力会很大。通常我会按实时性要求分开处理。实时性要求高的比如“热门商品Top100”可以用Redis里的zset维护每个商品的实时计数查询时直接读缓存。实时性要求中等、又需要明细级数据的可以在分库分表之上加一层汇总表用定时任务定期把各分片数据聚合进去。实时性要求不高、数据量又巨大的建议同步到数仓体系里做离线和准实时分析。这里有个实际经验值得分享在分库分表环境里做全量聚合时尽量把SQL改成“按分片并发扫描”然后由应用层做合并。别用框架自身的归并功能处理太复杂的聚合因为某些SQL写法会让框架退化成全分片扫表性能不可控。5.5 常见问题速查表为了方便遇到问题快速定位我把几个高频问题整理成一个速查表症状、原因、方向都在里面。| 现象 | 大概率原因 | 排查/解决建议 | | 部分查询很慢 | 查询条件没带分片键走了全分片路由 | 检查执行计划确认是否命中分片键 | | 某分片数据明显偏多 | 分片键分布不均匀或路由策略不当 | 重新评估分片键必要时重新设计分片算法 | | 迁移后数据对不上 | 增量同步漏数据或消息重复 | 增加主键幂等校验执行全量校验 | | 扩容后大量数据迁移 | 取模分片算法导致重新哈希 | 评估是否改用一致性哈希 | | JOIN查询超时 | 跨分片JOIN导致大量数据拉取 | 尝试绑定表、冗余字段或应用层合并 | | 统计报表数据异常 | 各分片汇总口径不一致 | 统一聚合口径增加离线核对任务 | | 消息消费重复导致数据重复 | 事务消息/本地消息表实现不严谨 | 消费端做去重按业务主键幂等 |这张表没法覆盖所有场景但可以帮你把排查思路理顺。分库分表系统里的问题大多数不是单一原因最好先看路由日志再看SQL改写后的执行SQL最后配合数据分布情况综合判断。6. 最后再说几句我的体会分库分表这件事我做了几年最深的体会是它不是一个纯技术问题而是一个成本决策问题。每引入一层分片逻辑系统复杂度都会上升一个台阶未来的运维和排查难度也会增加。所以我在项目里给自己定了一个原则能不分就不分要分就分得克制。能读写分离解决就不要急着拆库能按月归档解决就不要急着水平拆表非拆不可再认真选分片键认真设计迁移方案。另外最近我在帮朋友改造一个老系统时发现他们还在用“分库分表”来掩盖SQL本身写得差的问题一条查询动辄三四张表JOIN索引也没建好。把SQL压到单分片后再看发现很多问题其实不需要依赖分库分表也能解决。这个顺序一定不能反。如果你正准备启动分库分表项目我的最后一个建议是先把扩容、迁移、校验这三件事钉在计划里不要觉得“上线时候没问题就行”。分库分表是一项长期工程数据总是在涨的流量总是在变的提前把后续运营的路径想清楚比上线那一刻的爽快重要得多。希望这篇第三篇的实战内容能帮你少踩几个坑。