在做订单类系统时,业务方经常一上来就喊“表太大了,分库分表吧”。我通常不会马上开始拆,而是先去看一组数据:单表数据量有多少、单实例的CPU和IO压力有多大、高峰期慢查询占比高不高。只要这三个数字没到临界点,强行分片的结果往往是查询没变快,反而把路由、事务和扩容的复杂度全背到自己身上。
所以围绕“分表分库分片策略选型”,我想把一份从实际项目里沉淀下来的清单完整展开。会聊垂直拆分和水平拆分的边界、范围分片和哈希分片的适用场景、分片键怎么选、中间件怎么评估,以及扩容迁移时最容易翻车的几个细节。这篇文章不是教材,是一份可以直接拿去评审和开工的实操清单。
1. 先想清楚:什么情况下才需要动用分库分表
1.1 别把分库分表当默认选项,先看这三个硬标志
刚接手一个订单系统时,业务方上来就说“表太大了,要分库分表”。我的习惯是先不看要不要拆,而是先看三个数字:单表数据量、单实例CPU/IO压力、高峰期响应时间。
如果单表数据量还没到千万级,CPU利用率平时也就20%,慢查询主要是缺索引,那你真正要做的可能是加索引、做归档、上缓存,而不是拆表。我见过太多把一张300万行的表拆成32张,结果查询没变快,反而因为跨片查询和事务复杂度把自己绕进去了。分库分表真正要解决的,是单库和单表已经扛不住的问题,而不是设计上的“未雨绸缪”。
哪些算“扛不住”?我的判断标准有三条:一是单表超过几千万行后,索引层级加深,写入和查询的随机IO明显变贵;二是单实例连接数被打满,业务还在持续扩张;三是慢查询数量随着数据量线性上涨,即使加了索引也压不住。这时候才需要考虑把数据拆出去。
1.2 分库分表解决的三件事,和它换来的代价
分库分表本质上是拿复杂度换容量和并发。它一次解决三件事:存储容量、连接数瓶颈、单个分片上的读写压力。
但代价也摆在明面上:原来一条SQL能搞定的事,现在要考虑路由、聚合、排序、事务、自增ID。原来一个库备份恢复很简单,现在几十个分片要一起管理。原来扩容就是加硬盘,现在扩容要把数据重新打散。换句话说,做分库分表之前,脑子里得先有一本“复杂度账本”,否则拆到一半会发现,所有简单问题都变难了,又没有能力把难问题收拾干净。
我第一次带团队拆分订单库时,中间件选型花了一周,结果一个分片键选错,上线两周就要补一张映射表救场。所以这篇先把策略讲透,再谈选型,顺序不能反。
2. 拆库还是拆表:垂直拆分和水平拆分的边界在哪
2.1 垂直拆分:按业务域把库切开
先解释一个容易混淆的概念:分表和分库是两个动作,但经常一起出现。垂直拆分是按业务域把不同表放到不同数据库,比如用户库、订单库、支付库、商品库各拆一套。
垂直拆分解决的是“一个库承担了太多业务”的问题。所有业务表都在一个库里时,某个核心表的慢查询会把其他业务的连接池拖垮,跨业务的连接泄漏也会互相影响。拆成独立库以后,每个业务域独占连接和IO,扩容和故障隔离都更清晰。
但垂直拆分有一个显而易见的天花板:它并不能解决单表数据量还在增长的问题。订单库拆出去了,可订单表该有几千万行还是几千万行。所以垂直拆分在我看来更像第一步,它适合“业务域之间存在资源争抢”的场面,而不是“单表已经大到跑不动”的终局方案。跨库JOIN也随之出现,原先一个JOIN能搞定的事,现在要么在应用层做二次查询,要么引入宽表或聚合数据。
2.2 水平拆分:按数据行把表摊开
水平拆分才是真正把单表数据量压下去的手段。它把同一张逻辑表按照某个规则拆成多张物理表,比如把订单表拆成order_0000、order_0001……每一张表只保存一部分行,单表体积和索引深度都能控制住。
实际工程里,分库和分表经常叠加使用:先按业务域垂直拆,再对核心大表做水平分片,两个方向不冲突。水平拆分的关键在“规则”,也就是分片策略。规则设计得好,每个分片的数据量、热点、扩展性都舒服;规则设计得差,就会出现某个分片撑爆、其他分片闲着,等于白拆。
很多人在这一步就开始纠结中间件选型,我反而建议先把规则写清楚:到底用什么字段分、按多少片分、数据倾斜了怎么补救。规则定了再去选工具,工具只是执行规则,而不是反过来让工具决定你的架构。先想清楚这两个拆法的适用边界,后面选型才不会跑偏。
3. 分片策略选型:四种主流策略怎么挑
3.1 范围分片:简单直观,但小心热点堆积
范围分片是最好理解的一种:按某个字段的连续区间把数据切到不同分片。比如订单按创建时间切,1月的数据放order_202501,2月的数据放order_202502;或者按用户ID区间切,user_id 1-1000放一个分片,user_id 1001-2000放另一个分片。
优点非常明显:实现简单,适合时间维度归档;范围扫描友好,比如查询“某个月所有订单”可以直接落到对应分片,不需要扫全部片。我做过的一个日志系统就用时间范围分片,每天一个分片,过期数据直接降存储,非常省心。
缺点同样致命:数据热点容易集中。如果按时间切,那当前时间点产生的所有写流量都涌向最后一个分片,前面的分片闲得发慌,后面的分片被写垮。业务数据一旦有明显的“只写最近”特征,范围分片会制造一个天然的热点分片。按用户ID区间切也一样,新用户ID不断变大,新区间分片永远是热点。
所以范围分片更适合“时间序列、冷热分明、读写相对均衡”的场景。用在交易类核心表上,除非你能接受热点分片单独扩容,否则是下策。
3.2 哈希分片:均匀分布的常选方案,但扩容是硬伤
既然范围分片怕热点,那就用哈希把数据打散。常见的做法是取分片键的哈希值,再对分片总数取模,例如hash(order_id) % 16,这样数据在大概率上能均匀分布到16个分片。
如果分片键本身就是数字且分布均匀,比如用户ID,也可以直接user_id % 16;但要注意,直接取模对分片键的分布质量要求很高。用户ID如果是从1连续递增的自增ID,取模后基本均匀;换个不规则的号码却可能分布极差。更稳妥的方式是先做一次哈希散列,再来取模。
哈希分片最大的痛点是扩容。原来16个分片,数据按%16打散;要扩到32个分片,规则变成%32,几乎所有数据行的路由结果都会变。这就意味着扩容时必须全量迁移、重算路由,而不是像范围分片那样只动新增区间。为了缓解这个问题,才有了后面的“预分片”和“一致性哈希”思路。
哈希分片是业务交易系统里最常见的选型,因为流量均匀是第一诉求。但在选它之前,一定要先想清楚未来三年有没有扩容计划,以及你愿不愿接受迁移成本。我的建议是分片数一次性规划得大一些,宁可初期分片冗余,也别留一个三年后必炸的扩容坑。
3.3 一致性哈希:扩容友好,但要理解虚拟节点
一致性哈希是在“减少扩容迁移量”这件事上比普通取模聪明得多的方案。它把整个哈希空间看成一个环,分片节点落在环上,数据也按哈希值落在环上,然后顺时针找最近的节点存储。节点变化时,只有环上相邻区间内的数据需要移动,而不是全量迁移。
真实项目里还会引入虚拟节点。因为节点少时,哈希环上的节点分布可能很不均匀,某个节点会承担远超平均的数据量。引入虚拟节点后,每个物理节点对应几十个甚至上百个虚拟节点,让环上的数据分布更平滑。
不过我要泼一盆冷水:一致性哈希在数据库分库分表场景中,并没有像缓存场景那样普及。原因是它让数据位置不再是一个简单可计算的公式,路由的推导复杂了;跨分片查询时,很难通过分片键快速确定目标节点。它更多用在缓存、任务分配、消息队列消费组等节点频繁变化的场景。如果你在做分库分表选型,把它当成“了解但慎选”的选项即可,除非你的分片节点确实要频繁上下线。
3.4 时间分片与组合策略:别让路由变成一座迷宫
实际项目里很难用单一策略解决所有问题,所以我更建议做组合策略。比如“时间范围 + 业务维度哈希”:日志表先按月切分,每个月再按tenant_id哈希成4个分片,这样既能按月归档,又不会让单月数据集中在同一个分片。
组合策略要特别注意路由的可解释性。查询时必须能根据查询条件快速定位到少数几个分片,如果条件里没有时间或没有租户ID,路由只能扫描所有分片,性能直接退化成全表扫。我在一次选型复盘里见过一个案例:分片规则是“年份取模 + 城市哈希”,结果运营查数据时经常只提供用户昵称,没有年份也没有城市,每个查询都触发全分片扫描,线上慢查询瞬间爆表。
关于时间分片,还有一个细节:时钟回拨或业务数据补录。历史数据如果被重新写入,修改时间变了,路由位置也跟着变,容易造成同一行数据在不同分片之间漂移。处理办法是把分片键用“创建时间”而不是“修改时间”,并且在规则里固化“数据归属分片后不迁移”的原则。策略的复杂度可以高,但查询路径不能迷雾重重。
4. 分片键选型:这一环做错,后续全是补丁
4.1 一个好的分片键,至少要满足四个特征
分片策略定了,接下来就是选哪个字段当分片键。这一步我习惯用四个特征来过滤候选字段:区分度足够、分布均匀、不会频繁修改、查询条件里高频出现。
区分度不够的字段,比如只有“男/女”两种值,分片只能切成两堆,热点一眼可见。分布不均匀的字段,比如地区ID,发达城市数据量远超小城市,分片照样倾斜。频繁修改的字段,比如用户手机号,一旦换号就要把数据行迁到另一个分片,代价极高。查询条件里不高频的字段,比如只在后台报表里用的字段,用来分片会让前台查询全部跨片。
以订单表为例,候选字段有user_id、order_id、shop_id、create_time。如果是 C 端订单系统,user_id通常是首选,因为用户查自己的订单是最常见的路径;如果是 B 端商家后台,shop_id反而更合适。选分片键不是在选“最全局的字段”,而是在选“最高频查询路径里的稳定字段”。
4.2 如果主查询条件不是分片键,就用基因法兜底
很多时候你确实不得不用user_id分片,但业务里又经常出现“按订单号精确查询订单”的场景。订单号和用户ID怎么对齐?最土的办法是建一张order_id -> user_id的映射表,先查映射再路由,但多一次查询总让人不爽;更聪明的做法是基因法。
基因法的思路是把用户ID的部分信息“注入”订单号。比如用户ID取模得到分片号user_id % 16 = 3,生成订单号时让订单号的后4位也携带这个分片信息,这样拿到任何订单号,都能直接从订单号本身算出它应该落在哪个分片,不需要先查映射表。代价是订单号不再是简单的自增序列,长度可能变长,可读性变差。
基因法牺牲了一点优雅,换来了路由性能。类似方案还有“冗余分片键”,比如订单表既存order_id又冗余user_id,按订单号查时先算出user_id落点再访问分片。这些都属于分片键选型问题的补救措施,但如果你在一开始就把分片键选对了,补丁可以少打很多。
4.3 分片键改不动时,唯一有效兜底:冗余路由表
如果分片键已经上线,业务又要求支持按另一个字段查询,比如经常按“手机号+订单号”组合查,不要试图把所有查询条件都塞进分片规则,那样会陷入路由平衡的怪圈。我建议维护一张冗余路由映射表,这张表记录“业务标识 -> 分片编号”,查询时先查路由表再访问真实分片。
路由表本身数据量不大,可以单库存放,写入在业务创建数据时同步维护,读取可以先走缓存。它多引入了一次查询,但把路由复杂度控制在了可接受范围。我见过太多团队为了让两个查询条件都能直接路由,把分片规则做成“先按A分片,再按B分片”,最后查询时只要缺一个条件就得扫全部片,属于典型的过度设计。
5. 组件选型:分片规则交给谁执行
5.1 中间件形态怎么选:客户端型还是代理型
分片策略只有落到执行层才有意义。这一步的主要选择是在客户端型中间件和代理型中间件之间做取舍。客户端型中间件以代码库形式集成进业务应用,应用通过它访问数据库,分片逻辑由它在驱动层完成;代理型中间件以独立服务形式部署,业务应用连接的是代理,代理再把请求转发给真实数据库。
两种形态没有绝对好坏,关键看团队掌控力。客户端型部署轻、性能开销小、SQL兼容性可以做到很细,但升级要改业务应用,排障要看应用日志;代理型集中治理、对业务代码侵入小,适合跨团队统一管理,但多一层网络转发,会增加一点延迟和高可用复杂度。
我做分库分表选型时有个原则:团队越早期,越优先客户端型,因为它灵活,方便在策略上快速迭代;团队规模大了、多个业务线共用数据库基础设施时,才引入代理型做统一接入。不要一上来就搭一个大而全的数据访问层,选型越重,调整分片策略的成本就越重。
5.2 一张功能清单,才是真正的选型表
工具名只是表象,真正要对比的是功能清单。我给团队做过一次标准评估维度,整理出来就是下面这张表:
| 选型维度 | 要重点确认的问题 |
|---|---|
| SQL兼容性 | 子查询、多表JOIN、分页、聚合函数是否被支持或改写 |
| 分布式事务 | 是否支持XA、TCC或本地消息表,业务强一致等级能否满足 |
| 读写分离 | 是否支持多主多从、读写压力分离后路由规则是否可自定义 |
| 分片策略 | 是否原生支持Hash、Range、按时间、自定义分片算法 |
| 扩容工具 | 是否提供在线迁移、分片合并/拆分、数据一致性校验工具 |
| 监控能力 | 是否能看到每个分片的请求量、慢查询、连接数 |
| 团队成本 | 引入后需要多少人能玩转,排障是否需要专门专家 |
我特别提醒:SQL兼容性要拿真实业务SQL去测,而不是看文档。很多中间件宣传支持标准SQL,实际跑业务里那种几百行的报表SQL时,改写得一塌糊涂。选型前整理一份业务高频SQL清单,覆盖增删改查、分组排序、分页、多表JOIN,让备选方案全部跑一遍,比读十篇对比文章都管用。
选型过程还要做一个“分片策略演练”:把线上流量做一次压测回放,观察中间件的CPU、内存、连接数,看它在高峰期的表现。工具最终是辅助,做决定以前一定要先验证分片策略在工具上的实际表现,而不是只看功能列表。
6. 扩容与迁移:从双写到灰度切流
6.1 三种扩容方式的利与弊
分库分表之后,扩容一定是绕不开的问题。常见的三种方式是停机迁移、双写双读、平滑迁移。
停机迁移适合老项目能接受短暂停服的场景,做法是停流量、备份数据、按新规则重排、重启服务。它能保证数据一致性最简单,但停服时间跟数据量成正比,数据量一大,时间窗口就不可控。
双写双读是主流平滑方案:新老两套结构并存,业务先把写操作同时发给老库和新库,再通过异步任务把历史数据回填到新库,数据校验通过后,把读流量逐步切到新库。双写听起来简单,细节却很磨人:老库更新成功而新库失败怎么办、两边的自增ID冲突怎么办、数据校验用什么口径对账,这些都算常见问题。
平滑迁移的核心是灰度:切流不要一次切100%,先切5%的流量试运行,观察错误率和延迟,确认稳定后再逐步放大。我在一个订单项目里是分了三轮切流:第一轮切内部测试账号,第二轮切5%线上读流量,第三轮等校验一致后再切全部写流量。顺序一旦搞乱,回滚成本会非常高。
6.2 预分片:用一层桶映射把扩容成本降下来
预分片是很多团队容易忽略的思路。它的做法是提前把逻辑分片数设大,比如一次性分成1024个逻辑分片,然后用一张“桶映射表”把逻辑分片映射到物理实例:
| 逻辑分片范围 | 所在的物理库 |
|---|---|
| shard_0 ~ shard_63 | order_db_0 |
| shard_64 ~ shard_127 | order_db_1 |
| shard_128 ~ shard_191 | order_db_2 |
业务写入时,先根据分片键的哈希值确定落到哪个逻辑分片,再通过映射表找到物理实例。扩容时不需要重算业务数据的哈希位置,只需要调整映射表,把一部分逻辑分片挪到新的物理实例上。映射表可以在内存里维护一份,也可以做成配置下发,运维变更就成了改配置文件而不是全量迁移。
预分片的缺点是初始就有很多空闲分片,管理成本略高,比如连接池、备份任务都会随之增多。但它换来的扩容平滑性太值得了。我的默认建议是:分片数往未来三年的数据量估,不要图省事只分16片,否则后面每次扩容都是一次大迁移。
6.3 迁移中的三个硬核校验点
迁移过程中最容易出错的是“对账”这一环。我通常会在回填完成后做三件事:总数校验、抽样校验、变更流校验。总数校验比对老库和新库各分片的数据行数;抽样校验抽查特定ID在老库和新库中的字段值是否完全一致;变更流校验则观察回填期间新产生的数据,是否通过双写机制准实时同步到新库。
这三个校验全部通过后,才轮到切读流量。即使如此,我也建议把新库的慢查询监控和失败率监控提前接好,一旦切流后出现异样,能立刻在监控上看到,而不是等业务投诉。迁移最大的坑不是工具不给力,而是没有定义“什么才算迁移成功”的验收标准,导致数据都切完了,团队心里还没底。
7. 分库分表后的经典痛点与排查清单
7.1 分布式事务:先问业务能不能接受最终一致
分库分表后,原来一个本地事务里更新订单和扣库存,变成跨库跨服务操作,本地事务已经管不住了。处理方案常见的有XA强一致、TCC补偿、事务消息和本地消息表。
我的建议是:先问业务上的强一致性要求到底有多强。很多场景根本不需要分布式事务,而是可以改成“先写一条状态数据,再异步处理”;应用层通过消息表记录事件,消费端做幂等处理,出现问题就重试,最终能达到业务一致。真正需要强一致的场景,比如账户扣款,才值得引入TCC这种方案,因为它的开发成本和排障难度都不是一般团队愿意承担的。
工具能开箱提供分布式事务是加分项,但在选型时一定要看它的事务协议和业务侵入程度,避免用了一个自研很重的事务框架,改不动、也查不了。
7.2 跨分片查询与分页:能汇总尽量汇总,别硬扛
分库分表之后,一条SQL本来能完成的排序分页,现在必须从每个分片分别查出数据后在应用层合并,这也带来两个麻烦:排序要重排,深度分页要把数据全拉出来再翻页,否则会变成慢查询。
这不是完全无解。常见做法是把需要跨片聚合的数据沉淀到汇总表或宽表:比如订单统计报表,每天由定时任务从各分片汇总到统计库,查询直接打统计库;再比如全文检索需求,数据同步到搜索系统,后端检索不直接扫分片。
如果你只是想按订单状态查列表,还可以考虑“分页路由优化”:只查目标分片而非全部分片,前提是你能把查询条件转换成确定的路由条件。这个前提往往不成立,所以对大多数团队来说,设计时就该意识到,复杂分析型查询不要期望在分片数据库上硬扛,而是设计好数据出口。
7.3 全局唯一ID:别用自增ID硬扛
分库分表后,每个库各自维护自增ID,不同库之间会出现重复ID,全局唯一ID就成了必选项。最常见的方案是雪花算法:64位里包含时间戳、机器ID、自增序号,趋势递增且全局唯一。另一种是号段模式:数据库集中生成一批号段,业务应用拿号段后在内存里自增,性能和唯一性都兼顾。
雪花算法的坑主要在时钟回拨。一旦机器时间回拨,生成的ID就可能重复。成熟生产环境会用延迟等待、备用序列、时钟同步等方法兜底。号段模式的坑则是中心库的可用性和号段分配的高并发性能,需要做好缓存和熔断。我在项目里有时更倾向号段模式,因为它对团队理解成本最低,也不容易被时钟问题坑到。
7.4 问题排查速查表:一线摘出来的经验
分片系统出问题时,现象往往被中间件包装过,定位起来很费劲。下面这张表是这几年排查问题的高频路径:
| 典型问题 | 可能原因 | 排查思路 |
|---|---|---|
| 某个分片打满,其他分片空闲 | 分片键区分度差或值有明显热点 | 查每个分片的数据量分布,检查分片键冷热 |
| 慢查询数量飙升但单分片负载不高 | 跨分片扫描,缺少确定的拆分路由条件 | 打开中间件路由日志,分析SQL命中了几个分片 |
| 分布式事务经常回滚 | 事务实现中对业务幂等处理不到位 | 看事务参与者日志,重点对补偿逻辑做幂等验证 |
| 扩容后数据对不上 | 双写失败、回填任务丢失、映射表更新时机不对 | 按“总数校验、抽样校验、变更流校验”三项逐一排查 |
| 数据倾斜严重 | 分片键本身分布倾斜,或虚拟节点设置不当 | 统计分片键的TopN值,把热点值的路由单独拎出来处理 |
这张表不是万能药,但能帮你在问题发生时先缩减排查范围。平时也要把中间件的路由日志和分片延迟监控提前打开,否则出了事再临时加日志,现场已经被污染了,很难再还原。
8. 收尾:把选型清单沉淀成团队的固定动作
我做了这么多次分库分表,最大的体会是:策略选型比工具选型重要,分片键比中间件重要,迁移前的对账比迁移过程重要。很多人拿着“分库分表”四个字就开干,结果中间件、分片键、扩容全被推着重来一遍。
所以现在我做任何系统,都会强制先出一份分片策略选型清单:数据量预估、业务查询路径、分片键候选、分片策略对比、扩容计划、迁移验收标准。清单写完后,评审时第一时间请有经验的同行看分片键和查询路径,而不是只看工具名。这个动作看着朴素,却帮我挡住了至少三次灾难级的返工。如果你正要做分库分表,不妨先拿清单把方案钉下来再动手,别让“分片”两个字从一开始就变成一个深坑。