分库分表的取模算法策略,怎么避免数据倾斜?
一则或许对你有用的小广告
欢迎加入小哈的星球,你将获得:专属的实战项目(4个项目都能学) / 1v1 提问 / 简历修改 / Java 学习路线 / 社群讨论 / 学习打卡 / 每月赠书
《Spring AI 项目实战(问答机器人、RAG 智能客服、联网搜索)》已完结,基于
Spring AI + Spring Boot 3.x + JDK 21...,查看介绍《从零手撸:仿小红书(微服务架构)》 已完结,基于
Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...,查看介绍;演示链接:http://116.62.199.48:7070/《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接:http://116.62.199.48/
新开坑项目:《从零手撸:秒杀系统高并发优化实战》 正在更新中...,查看介绍
截止目前,星球内专栏累计输出 150w+ 字,讲解图 5110+ 张,还在持续爆肝中.. 后续还会上新更多项目,已有 4700+ 小伙伴加入学习,欢迎点击围观
面试考察点
-
基础掌握度:面试官不光想知道你会不会写
hash(key) % N,更想知道你分片键该怎么选、取模算法的优缺点是什么,而不是只会抄一个公式。 -
问题意识:能不能识别出 “数据倾斜” 这类典型的分库分表陷阱,并且知道它为什么会发生——是分片键选错?基数太小?还是业务热点?
-
实战经验:生产中真正遇到过倾斜的场景是怎么解的?是调整分片键,还是引入一致性哈希,或者搞了冷热分离?能不能说出具体的取舍。
核心答案
先一句话定个调:取模分片本身是均匀的,但 “均匀” 只是数学上的均匀,真正的倾斜来源是分片键分布不均、热点 key、以及扩容方式不当。避免倾斜主要从四个方向入手:
| 方向 | 关键做法 |
|---|---|
| 分片键选择 | 选区分度高、分布均匀的字段(user_id、订单号) |
| Hash 算法优化 | 标准 hash 用 MD5/MurmurHash 取末几位,别用业务字段直接取模 |
| 扩容方式 | 分片数按 2 的幂扩容,或改用一致性哈希 |
| 热点治理 | 大 V / 热点商家单独分片、加随机后缀打散 |
下面细说。
深度解析
一、取模分片的本质与倾斜来源
取模分片的公式很朴素:
// 伪代码:分片定位
int shardIndex = hash(shardKey) % shardCount;
理论上,只要 shardKey 分布均匀、hash 函数够散,落到每个分片的数据量应该是大致相等的。但现实骨感,倾斜通常来自这三类:
上图把倾斜来源拆成了三大类,每类对应不同的应对手段:
- 分片键分布不均:这是最常见也最致命的,比如用
性别分片,无论怎么取模都只有 0/1 两个值,必然倾斜到某两个分片上 - Hash 散列性不足:直接拿业务字段(比如订单号的最后两位)取模,可能因为业务规则导致某些值聚集
- 业务热点:分片是均匀的,但某个大 V 用户单表数据量就顶别人 1000 个用户,这种是单分片内部倾斜
二、避免倾斜的具体策略
1. 选对分片键(最关键的一步)
分片键要满足两个硬条件:区分度高 + 分布均匀。
| 场景 | 推荐分片键 | 反例 |
|---|---|---|
| 用户中心 | user_id |
性别、注册时间 |
| 订单系统 | user_id 或 order_id |
订单状态、支付方式 |
| IM 消息 | sender_id 或 conversation_id |
消息类型 |
这里有个经典坑:很多人喜欢用 create_time 做分片键,觉得按时间分方便归档。但同一个时间窗口的订单会全部集中到同一张表,写入热点严重。除非是冷热分离场景,否则别单独用时间分片。
2. Hash 算法的优化
直接 shardKey.hashCode() % N 在很多场景下不够散,更稳妥的做法是:
// 推荐做法:用 MD5 散列后取末几位
String hashSource = String.valueOf(userId);
byte[] md5 = MessageDigest.getInstance("MD5").digest(hashSource.getBytes());
// 取 MD5 结果的后几位作为分片依据
int shardIndex = Math.abs(md5[md5.length - 1]) % shardCount;
ShardingSphere 默认就支持 MD5、MurmurHash 等多种散列算法,说白了就是让原本相邻或相似的分片键,经过 hash 后尽量分散开。
3. 扩容时的倾斜治理
取模分片最大的痛点是扩容,从 4 库扩到 6 库,几乎 75% 的数据都要迁移。两种主流解法:
- 翻倍扩容:始终让分片数保持在 2 的幂(4、8、16、32),每次扩容都是翻倍,数据迁移量稳定在 50%。这是工业界用得最多的方案。
- 一致性哈希:把整个 hash 空间想象成一个环,节点变化时只影响相邻段的数据。迁移量最小,但实现复杂,且容易在小规模场景下分布不均(需要加虚拟节点)。
4. 热点数据的特殊处理
这是取模分片无法解决的问题。比如某个大 V 的 user_id = 10086,不管你怎么取模,他自己的数据永远落在同一个分片上。如果这个用户的数据量是其他用户的 1000 倍,那这个分片就是倾斜的。
常用解法:
- 拆表 + 加随机后缀:给热点 key 加上随机后缀(比如
10086_0、10086_1…10086_9),强制打散到 10 个分片 - 独立分片:把大 V 用户单独路由到一组独立分库(实际就是按 uid 做白名单路由)
- 冷热分离:活跃数据进热库,历史数据归档到冷库(HBase、TiDB 等)
5. 引入二级分片
对单表数据量极大的场景,用 “分库 + 分表” 两层结构:
// 一级:按 user_id 分库
int dbIndex = userId.hashCode() % 16;
// 二级:在库内再按 create_time 分表
int tableIndex = (int)(orderId % 4);
这样既能保证用户维度聚合在同一个库,又能让单库内的订单按时间分散到多张表,避免单表数据量爆炸。
面试高频追问
-
追问一:如果分片键选错了,已经上线了怎么办?
- 老老实实做双写灰度迁移:新表用新分片键写入,老表继续读;异步把历史数据补齐到新表;切换流量;最后下线老表。整个过程可能持续 1-2 个月,做好回滚预案。
-
追问二:一致性哈希和取模分片怎么选?
- 数据规模稳定、扩容频率低 → 取模分片(简单可控)
- 节点经常变动、需要动态扩缩容 → 一致性哈希(迁移量小)
- 阿里、美团的内部中间件很多用预分片 + 路由表,相当于把两种方案的优点都拿了。
-
追问三:分库分表后,跨分片的查询怎么做?
- 能避免就避免(业务设计上就规避)。实在要查,就广播表(小表如字典表,每个分片都冗余一份)或者异构索引(用 ES、HBase 做二级索引)。
常见面试变体
- “分库分表都有哪些分片策略?各自适用什么场景?”
- “为什么一致性哈希能减少扩容时的数据迁移?”
- “分库分表后如何处理跨库 JOIN 和分页?”
- “如何评估分库分表的数量?分多少合适?”
记忆口诀
四字诀:键、hash、倍、热。
- 键:分片键要选区分度高、分布均匀的
- hash:用 MD5/MurmurHash 散列,别裸取模
- 倍:扩容翻倍,迁移量可控
- 热:热点单独治理,别指望取模解决一切
总结
一句话:取模分片看似简单,但真正的功力在分片键的选择和热点治理上。把 “选对键、用对 hash、翻倍扩容、热点单独处理” 这四条记住,再结合实际业务场景给出取舍,面试官追问到哪一层你都能接住。
