单表卡到一两千万行的时候,很多团队的第一反应就是上分表分库。这种心情我特别理解,当年我们订单表刚过三千万,一个带条件的 count 查询能把主库 CPU 打到 80%,DBA 半夜打电话把我叫醒,张嘴就是“分了吧”。但真到了选分片策略的时候,才发现最难的其实不是中间件怎么配,而是面对 Hash、Range、一致性哈希、映射表这一堆方案,你根本不知道自己的业务该抄哪一份作业。
这篇文章就是一份可以直接拿来对照的“分表分库分片策略选型清单”,我尽量把每个策略的适用场景、坑点、选型参数都讲透,适合正在做库表拆分方案预研的架构师,也适合被领导派去“调研一下分库分表方案”的后端开发。内容不会涉及具体中间件的安装教程,重点放在策略本身的权衡逻辑上——因为不管你最后用 ShardingSphere、MyCat 还是自研路由,底层选的还是同样的分片策略。
先说个反常识的结论:分库分表解决的是“数据量大导致的操作慢”,但它同时会把“简单查询变复杂”“事务范围变窄”“扩容变重”。所以选分片策略之前,先得有清单确认自己是不是真的走到了那一步。
1. 先别急着分库分表:动手前的评估清单
1.1 什么信号说明“必须分了”
不是所有慢查询都需要分库分表。我见过最典型的误判,是团队把一条没走索引的 SQL 当成“数据量太大”,结果分完库之后该慢还是慢,反而多了一堆分布式事务的麻烦。真正需要拆分的信号,通常同时出现两到三个:
单表行数超过千万级,且业务增长曲线看不到头。这个阈值不是拍脑袋,机械硬盘时代单表超过两千万行,B+ 树高度到四层,随机 IO 成本明显上升;现在 SSD 稍微好一点,但超过一亿行之后很多聚合操作依然会明显变慢。
主库写入成为瓶颈。单库的写入能力受限于磁盘 IO 和 binlog 复制延迟,读写分离只能缓解读压力,解决不了写入单点。如果你的写入 TPS 长期超过单库承受能力,或者主从延迟在工作日高峰期持续拉高,这时候单纯加缓存已经没用了。
数据归档做了还是慢。很多团队先试了按月归档、按用户归档,发现业务查询范围太宽,归档后的数据依然常被访问,冷热分离扛不住热数据的体量,那这一步就该考虑分片了。
有一个原则很重要:分库分表解决的是“量大”的问题,不是“SQL 写得烂”的问题。如果慢查询日志里全是全表扫描和文件排序,先花两周优化索引和 SQL,可能省掉半年拆分的成本。
1.2 分库分表能解决什么、解决不了什么
列一张表会清楚很多。这也是选型清单的第一张表,建议直接贴到方案评审的文档里。
| 问题类型 | 分库分表能不能解决 | 说明 |
|---|---|---|
| 单表数据量过大导致读写性能下降 | 能 | 数据分散到多个分片,单分片数据量下降 |
| 单库写入TPS瓶颈 | 能 | 多库并行写入,前提是分片键分散合理 |
| 单次查询过大(大范围扫描) | 部分能 | Range分片可优化,Hash分片对大范围查询不友好 |
| 跨分片 join / 聚合 | 不能 | 需要应用层组装或预聚合,复杂度大增 |
| 分布式事务 | 不能 | 跨分片事务成本极高,最好从业务设计上规避 |
| 深分页 order by limit | 不能 | 需要全局排序,分片越多性能越差 |
| 热点数据集中访问 | 部分能 | 取决于分片键设计,热点分片依然会被打爆 |
| 非分片键查询 | 不能 | 需要映射表或搜索引擎辅助 |
看明白这张表,你就知道为什么很多大厂最后走的是“分库分表 + 异构数据同步”的路子:分片只解决主链路问题,非分片键查询丢给搜索引擎或宽表去扛。
1.3 先做低成本优化,再考虑分片
还有一个容易忽略的问题:成本。从单库到分库分表,代码改动不只是一层 DAO 层的路由替换——SQL 改写、分页重写、事务边界重构、数据迁移脚本、灰度发布方案、回滚预案,每一项都是工期。按照我见过的团队节奏,一个核心业务立项到平稳上线,最少得一个季度,这还不算后续半年里陆续冒出来的边界 bug。
所以在评估阶段,建议按顺序排除以下方案:
- 慢 SQL 治理和索引优化
- 缓存兜底热点读(Redis 等)
- 归档冷数据到历史库
- 读写分离扛读压力
- 垂直拆分(按业务域拆库),先不搞水平分片
这些方案成本低、见效快,而且会为以后的分库分表打下基础——比如你先把订单和商品拆到不同库,后续再做水平拆分时,至少不用同时解决跨业务域的问题。
2. 核心选型清单:五大经典分片策略逐个拆解
选分片策略本质上是回答三个问题:数据怎么均匀散开?查询怎么定位到具体分片?扩容的时候数据怎么迁?围绕这三个问题,业界沉淀出了几种经典方案,下面逐个讲。
2.1 Hash取模分片:最直接,但扩容是硬伤
Hash 分片是默认选项,也是最容易理解的一种。选一个分片键,比如用户 ID,计算哈希值后对分片总数取模,路由公式大致长这样:
// 分片总数 = 库数 * 每库表数 int dbIndex = Math.abs(hash(userId)) % dbCount; int tableIndex = Math.abs(hash(userId) / dbCount) % tableCount;这种两层路由的做法很常见:第一层用userId % dbCount选库,第二层用userId / dbCount再取模选表,能保证同一个用户的数据落在一个库的一张表里,不会出现“同一个用户订单散落在多个库”的尴尬情况。
Hash 分片最大的优点是数据分布均匀。只要分片键的取值离散度够——比如自增 ID、随机串——每个分片的数据量和写入压力都差不多,不会有明显短板。它的缺点是范围查询和批量操作基本报废。你想查某个用户最近三个月的订单,前提是 SQL 里必须带上用户 ID;如果只按时间查,中间件就得对所有分片广播查询,再把结果合并排序,性能可能比不分片还差。
更要命的是扩容问题。原来的分片总数是 8 个库,现在要扩到 16 个库,取模的模数变了,几乎所有的数据映射关系都要变,意味着你要全量迁移数据,而且迁移过程中还得保证业务不停。这就是很多团队在 Hash 分片后死活不敢扩容的原因。
实操心得:如果你确定未来一两年内数据量不会翻倍,Hash 取模是最省心的方案,维护成本低、读写均衡,适合大多数 ToC 业务的用户主链路。
2.2 Range范围分片:公认最懂业务的方案
Range 分片的思路是告诉中间件“哪一段数据去哪张表”,通常按时间或 ID 区间划分。比如订单表按月份分表:order_202401、order_202402,时间条件明确时可以精准路由到某一张表,范围查询天然友好。
Range 分片最典型的场景就是时序类数据:交易流水、日志、操作记录、消息历史。这类数据有两个特点:查询总是带时间范围,而且旧数据访问频率越来越低。这时可以结合归档策略,把一个季度前的分片自动降级到冷存储,查询路径和存储成本都好看。
但 Range 分片有个致命软肋:热点集中在最新分片。所有新订单都写进当月的表,月底最后一天那张表可能扛着整个系统的写入压力,其他分片却很闲。层主库的“月底大促”场景里,你要是按天分表,峰值那天照样会被打爆。
折中的做法是“时间 + 业务维度”的组合分片。比如先用卖家 ID 做 Hash 分库,再用月份做 Range 分表,这样写入压力被卖家维度打散,查询又保留了按时间收敛的能力。
// 组合分片示意:库按卖家Hash,表按月份 int dbIndex = Math.abs(hash(sellerId)) % dbCount; String tableName = "order_" + yyyyMM;这种方案的代价是路由规则稍微复杂,但对大部分电商、支付类业务来说,性价比远高于纯 Hash 或纯 Range。
2.3 一致性Hash与虚拟节点:扩容友好的选项
一致性哈希值得认真理解,因为它解决的是 Hash 分片最痛的问题——扩容/缩容时的数据迁移量。
原理不复杂:把整个哈希值域组织成一个环(比如 0 到 2^32),每个分片节点在环上占据一个或多个位置。要定位一个分片键,就算它的哈希值,然后沿环顺时针找第一个节点。这样加节点时,只有环上该节点位置到前一个节点之间的数据需要迁移,其他数据保持不动。
但是朴素的一致性哈希有个问题:节点少的时候分布严重不均,运气不好可能出现“一个节点扛了 80% 数据”的情况。解法是引入虚拟节点——每个物理节点在环上放几十个甚至上百个虚拟位置,让数据分布更均匀,同时也能让不同性能的机器承担不同权重。
| 维度 | Hash取模分片 | 一致性Hash分片 |
|---|---|---|
| 数据分布均匀性 | 好 | 依赖虚拟节点设置,配置合理则均匀 |
| 扩容迁移量 | 近似全量 | 只迁移环上局部数据 |
| 路由复杂度 | 极低 | 中等 |
| 范围查询支持 | 差 | 差 |
| 运维心智负担 | 低 | 中高 |
一致性哈希在 DDoS 防护、缓存集群、负载均衡领域用得多,数据库分片里也有团队采用。它的核心价值是“随时扩缩容”,如果你的业务流量季节性波动很明显,需要定期加节点扛峰值、砍节点降成本,那一致性哈希比固定取模合适得多。
注意:数据库分片用一致性哈希,意味着中间件要自己维护虚拟节点和路由表。有些团队为了这个能力专门自研了分库分表组件,复杂度不低。小团队还是建议用成熟中间件的内置一致性哈希实现,别自己去造轮子。
2.4 映射表与目录服务:灵活但多一跳
映射表模式比较另类:不直接根据分片键计算路由,而是维护一张“分片键 -> 实际存储位置”的映射关系。比如用户有 1000 个订单,这 1000 个订单分别存在哪些库哪些表,在映射表里查一下就知道了。
这种模式的优点是路由规则可以随时改,数据迁移时映射表同步更新就行,业务代码无感知。多租户场景特别喜欢——每个租户的数据量差异可能非常大,按租户 ID 取模会导致大数据租户占用整个分片,而映射表可以手动把大租户的库表分配调匀。有些 ToB 系统的“企业版专属存储”就是拿映射表做出来的。
代价也很明显:查询多了一跳。原来的路径是“SQL 直连路由”,现在变成“查询映射表 -> 拿到物理位置 -> 再查实际分片”,延迟多了几毫秒,而且映射表本身成了新的单点和性能瓶颈。解决思路是把映射关系缓存到 Redis,量大时甚至可以本地缓存 + 版本号更新。
我的建议是:映射表不要全局一张,最好做成分片键维度的分片映射表,比如按租户 ID 分表存映射关系,避免单表过大。还有一种轻量用法——只把映射表用于“热点用户”的手动调度,普通用户走 Hash 取模,等于给分库分表加了一个人工干预的旋钮,灵活性和性能都能兼顾。
2.5 各策略适用场景对照与选型建议
把主流策略放在一张表里对比,选型时直接对着看。
| 策略 | 推荐场景 | 优势 | 劣势 | 优先级建议 |
|---|---|---|---|---|
| Hash取模 | 用户/订单等按ID等值查询为主的业务 | 均匀、实现简单 | 扩容难、范围查询差 | 刚起步的团队首选 |
| 一致性Hash | 流量波动大、需频繁扩容 | 扩容迁移量小 | 路由复杂、依赖虚拟节点 | 运维能力强可考虑 |
| Range范围 | 日志、流水、时序数据 | 范围查询友好、可分层归档 | 热点集中、月末峰值风险 | 时序场景必选 |
| 组合分片 | 电商订单、交易流水 | 均衡与查询兼顾 | 路由规则复杂 | 数据量大的核心业务推荐 |
| 映射表 | 多租户、数据倾斜严重 | 灵活、可人为干预 | 多一跳、映射表可能变瓶颈 | 配套方案,一般不单独用 |
这里多说一句:很多团队喜欢抄头部大厂的方案,但大厂的“一致性哈希 + 映射表 + 冷热分离”整套体系是为千万级 QPS 和上万台机器设计的。如果你的数据总量在几十亿以内,峰值写入不超过几千 TPS,用“Hash 取模 + 时间归档”就完全够了,简单方案能扛住 80% 的业务,剩下 20% 的问题遇到了再升级也不迟。
3. 实操阶段要做对的三件事:分片键、路由设计与容量规划
选了策略只是第一步,真正动手设计时,分片键的选择、路由的落地方案、容量的预估这三个环节才是决定成败的关键。这一章我按实操顺序一点点说。
3.1 分片键选型三问:选错了后面全是坑
分片键是整个分库分表方案的心脏,一旦定下来,后面几乎没法改。选型的时候问自己三个问题:
第一,这个键能不能唯一定位核心业务主体。订单表的核心业务主体是买家还是卖家?如果是买家,就选buyer_id;如果买卖双方都是主角,就得考虑“买家维度为主、卖家维度走辅助索引”的折衷。选错了,后续最常见的尴尬是:订单表按买家 ID 分片,结果运营后台全是按卖家 ID 查订单,每条 SQL 都得广播全库——那种痛苦用过的人都懂。
第二,这个键的取值够不够分散。性别、状态、渠道这种枚举字段千万不能当分片键,因为值就那么几个,数据根本散不开,会导致几个分片极不均匀。用户 ID、订单号、设备 ID、企业统一信用代码这类高基数字段才是候选。
第三,查询是不是总会带这个条件。分片键的价值在于路由定位,如果业务里大量查询根本不带这个字段,那分片对查询的加速作用就大打折扣,甚至变成负担。
说一个我踩过的坑:早年我们把订单表按order_id做 Hash 分片,逻辑上很完美——订单号唯一且均匀。结果上线两周发现,后台运营所有查询都是按商家维度拉的,因为订单号对运营来说毫无意义。每条 SQL 都要广播到 64 张表,再在应用层做内存合并,原本 50ms 的查询变成 2 秒,运营同事直接投诉。后来只能加了一张“商家-订单”的映射表,才算把问题缓解。这就是典型的分片键只考虑“技术均匀性”而忽略“业务查询维度”的教训。
3.2 路由与SQL改写:中间件的活儿自己得懂
关系型数据库的分片路由,现在主流是两大类:一类是应用层框架,比如 ShardingSphere 这种,嵌入到业务代码里做 SQL 解析和路由,优点是轻量、部署简单;另一类是独立部署的中间件,比如 MyCat 这类代理层,对应用透明,但多了一层网络转发和协议转换,性能损耗和运维成本都要考虑。
选哪种我的看法是:小团队优先选应用层框架,因为代理层自身的性能瓶颈、高可用配置就能让一个团队忙活好几周。但不管用哪种,有几个底层机制你必须懂:
精确路由(PreciseRouting)和范围路由(RangeRouting)。中间件拿到 SQL 后,会从 where 条件里提取分片键的值或范围,然后通过分片算法精确计算目标分片。如果 SQL 里没有分片键,就会走广播路由,给所有分片都发一份,再合并结果。这也是为什么“查询不带分片键”会变成性能灾难。
分片键加函数运算要小心。有些 SQL 喜欢写WHERE user_id + 1 = 100,绝大多数中间件无法从这种表达式里提取分片键,只能广播查询。类似的还有DATE(create_time) = '2024-01-01',如果 create_time 不是分片键还好,一旦分片键是它,建议把 SQL 改成范围条件,或者设计时就避免对分片键做函数包裹。
补充一个通用的路由计算公式示例,方便你评估中间件日志里的分片结果:
// 假设规划:10个库,每个库100张表,共1000个分片 // 分片键:sellerId = 9527 int shardCount = 1000; int shardId = Math.abs(String.valueOf(sellerId).hashCode()) % shardCount; int dbIndex = shardId / 100; // 决定落在哪个库 int tableIndex = shardId % 100; // 决定落在哪张表3.3 容量规划:先算清楚再定分片数量
分片数量不是越多越好。我见过一个团队把流水表分了 1024 张,结果单张表数据量才 20 万行,查询动不动就要跨 256 张表合并,路由效率低到令人崩溃。
容量规划的核心平衡点是:单分片的数据量要降到“单机舒服区间”,但分片总数又要控制在一个能接受的范围。经验公式大致是这样:
- 预估未来 2~3 年的数据总量,单位是行。假设订单每年增长 5 亿行,三年就是 15 亿。
- 确定单分片舒适数据量。对于 InnoDB,单表控制在 1000 万到 3000 万行之间比较稳妥,超过 5000 万就要考虑性能预警。
- 用总量除以单分片目标量,得到最小分片数。15 亿 / 2000 万 = 75 个分片,预留 20% 缓冲,取整到 128 个分片(8 库 × 16 表)会比较从容。
分片总数尽量保持“库数 × 每库表数”的结构,这样后续扩容可以按“库数量翻倍”或“每库表数翻倍”两个方向演进,不至于把底层路由表做得太零散。
3.4 扩容方案预设计:现在不想,后面更痛苦
很多人觉得扩容是几年后的事,到时候再说。实际上分片方案上线那一刻,扩容路径就已经被锁死了——所以设计方案时就要提前想好未来怎么扩。
如果是 Hash 取模分片,未来的扩容无非两条路。一条是翻倍扩容:库和表都翻倍,迁移时用双写 + 校验 + 切流的方式平滑过渡;另一条是提前在分片键上做二次取模,预先拆出“虚拟分片”,将来把若干虚拟分片归并到一个物理分片上。第二条路设计复杂,但迁移量小,适合数据量增长不可控的业务。
一致性哈希方案的扩容相对轻松:环上每个节点拆成两个虚拟节点组,一半数据平滑迁移到新节点,不过要提前做好虚拟节点的数量规划,不然迁移后热点依然存在。
实操心得:不管哪种方案,扩容都要准备一份回滚预案。最常见的手法是把数据迁移分为“同步阶段”和“切换阶段”,同步阶段全量+增量持续追平,切换阶段停写片刻、确认两边数据一致后改路由。如果切换后发现问题,路由改回去就行,前提是你保留了旧库的只读权限和数据快照。
4. 分片上线后的坑与周边系统联动
分库分表上线不是终点,它只是把单库时代的“大问题”换成了分布式时代的“一堆小问题”。下面是我们在实践中反复踩到的坑,直接列出来当速查表用。
4.1 常见问题速查表
| 现象 | 根因 | 排查思路 | 解决方向 |
|---|---|---|---|
| 某个分片数据量明显大于其他分片 | 分片键有热点值,比如超大商家 | 统计各分片行数和写入TPS | 热点分片再次拆分,或映射表手工引流 |
| 查询偶发超时,慢 SQL 出现在所有分片 | SQL 没带分片键,走广播路由 | 查看中间件路由日志 | SQL 改造带分片键,或加映射表 |
| 分页深翻页卡死 | 全局排序取 limit 100000,20 | 分析合并排序的开销 | 改成“游标分页 + 每分片取更多再归并” |
| 跨分片 join 返回数据错乱或超时 | join 键与分片键不一致 | 查看执行计划 | 冗余字段 + 应用层组装,或宽表 |
| 数据迁移后出现主键冲突 | 全局ID生成方案缺失或重复 | 对比迁移日志 | 使用Snowflake/Leaf/自研全局发号器 |
| 定时任务重复扫描同一数据 | 每个分片独立跑job,边界未切分 | 检查任务分片逻辑 | 按分片维度传入任务参数,而不是各自全扫 |
4.2 数据倾斜与热点处理
数据倾斜是分库分表上线后最让人头疼的问题。即便分片键整体均匀,也拦不住业务本身的头部效应——比如电商平台的大卖家、社交产品里的超级大 V。
处理思路分三层。第一层是分片键设计时就预埋“二级拆分”,比如商家维度再叠加商品维度,让超级大商家的数据进一步散开;第二层是动态识别热点分片,把热点数据复制到专门的热点库,查询命中时先走热点库;第三层才是事后的重新分片,成本很高。
很多团队会忽略一个要点:热点处理不是纯技术问题,先要知道热点怎么定义。是单分片行数超过阈值,还是单分片 QPS 超过阈值?这两个维度的处理方式完全不同。行数大意味着要拆数据,QPS 高意味着要加副本或缓存,别搞混。
4.3 非分片键查询的兜底方案
不带分片键的查询是无解的?也不是。兜底方案按成本从低到高排列:
- 映射表/索引表。在映射表里记录“业务键 -> 分片键”的关联,比如商家和订单的关系,查询订单时先查映射表拿到买家 ID,再走分片路由。适合低频后台查询。
- 搜索引擎/宽表。把全量数据同步到 ES 或 ClickHouse 之类的分析引擎,非分片键查询(组合筛选、聚合统计)直接走分析引擎。适合运营后台和 BI 报表。
- 异构索引。同步一份只包含“查询字段 + 分片定位字段”的轻量索引数据,体积小、查询快,但只能解决单点定位类查询,不适合聚合统计。
我接触过的业务,几乎都是“分片主链路 + ES 辅助查询”的组合拳。这里多说一句:全文检索和聚合统计用 ES 不是要同步全字段,只需要同步查询、过滤和展示必要的字段即可,尽量控制索引体积,降低同步延迟。
4.4 分布式事务与全局ID的配套设计
分库分表之后,单库事务变成了跨库“伪事务”。常用的方案是本地消息表、事务消息、TCC、Saga。没有一种是完美的,都拿“最终一致性”作为代价。
我最想提醒的是:分布式事务应该靠业务建模规避,而不是靠框架解决。在设计分片键时就把“需要在同一个事务里的操作”尽量放到同一个分片内——比如下单、扣库存、锁优惠券都围绕同一个用户 ID 展开,那它们天然落在同一个分片里,普通事务就能覆盖,根本不用引入分布式事务框架。我见过太多团队把订单和库存两个库拆开了,然后为了扣减库存和创建订单的一致性折腾了半年,最后发现业务上完全可以通过“预占 + 异步对账”来兜底,根本不需要强一致。
全局 ID 的坑也不小。分库分表后不能用自增主键,否则多个分片的主键必然冲突。主流的替代方案是 Snowflake 算法、美团 Leaf、滴滴 TinyID,或者直接上 UUID(注意存储优化和索引性能)。设计时要注意:全局 ID 不仅是主键,最好还能蕴含分片信息,比如把分片号编码进 ID 的中间段。这样反查 ID 时不用算哈希也能直接定位分片,排查问题的时候能省很多事。
4.5 灰度迁移与双写校验
最后讲数据迁移,这是整个分库分表项目最容易出事故的环节。很多团队栽在迁移上,不是分片策略选错,而是迁移过程没设计好。
标准做法是“双写 + 校验 + 切读”。双写阶段:旧库和新库同时写,新库的写入先通过改造后的 DAO 层路由过去;校验阶段:写一个对账任务,定期比对新旧库的数据差异,主键维度逐条比对;切读阶段:读流量按比例灰度,先放 10% 到新链路,观察错误率和耗时,逐步放大到 100%。
切读有个容易被忽略的细节:接口超时配置要分开。新链路首次承担大量请求时,冷数据加载、连接池预热都可能导致比旧链路更慢,如果你直接把超时时间调成和旧链路一样,灰度阶段可能会收到一堆超时误报。建议灰度开始时把新链路超时放宽 50%,稳定后再逐步收紧。
回滚方案必须提前定义清楚“回滚点”。比如灰度到 30% 的时候发现数据错乱,回滚操作是什么?路由切回旧库,双写继续保留,新库数据停更,等修复后重新同步——这套流程要写成剧本,并且排练一遍,而不是出了问题再开会讨论。
5. 我的一些总结性体会
分表分库分片策略的选型,本质上是在“查询灵活度、写入均衡度、未来扩展成本”这三个维度之间找一个能接受的平衡点,不存在一套方案通吃所有业务。我能给出的最具体建议是:先把业务查询维度清单列出来,按频率和重要性排个序,用这些查询去反推分片键和分片策略,而不是先定框架再让业务适配。顺序反了,后面全是返工。
最后再分享一个小技巧:不管选了哪个策略,上线之前一定要留一套“全量数据导出到单表”的工具。分库分表跑一阵子以后,你会发现所有的排查、对账、临时报表需求,最后都得靠这套工具把分散的数据重新拉回去分析。那时候回头看,这个不起眼的导出工具,可能是整个分片方案里性价比最高的一个组件。