数据分片原理:一致性哈希、范围分片、哈希槽
分片这件事,说白了就是把数据打散,分散到不同的节点上。但怎么打散,打散后怎么找回来,这里面的门道可不少。我这些年经手过的分布式系统,从 Redis 到 Cassandra 再到自研的存储中间件,几乎都绕不开这三种分片方式。
今天咱们就掰开揉碎了聊聊:一致性哈希、范围分片、哈希槽。这三种方案各有各的脾气,选对了事半功倍,选错了……嗯,扩容的时候你就知道疼了。
一、一致性哈希:最灵活,但小心偏斜
一致性哈希,我最早是在做缓存集群时接触到的。当时用的是一套老方案,加一台机器要迁移一半数据,搞得运维同学天天加班。后来换成一致性哈希,世界清净了。
它的核心思想很简单:把整个哈希空间组织成一个环。节点和数据都通过哈希函数映射到这个环上。数据找节点时,顺时针找第一个遇到的节点就行。
核心优势:增删节点时,只影响相邻节点的数据,其他节点纹丝不动。
举个例子,假设你有 3 个节点,哈希环上有 100 个位置。节点 A 在位置 10,节点 B 在位置 50,节点 C 在位置 80。数据 key 哈希到位置 35,顺时针找到节点 B,数据就存到 B 上。这时候加一个节点 D 到位置 60,原来属于 B 的一部分数据(位置 50-60 之间的)会迁移到 D,其他数据完全不受影响。
但这里有个坑——数据偏斜。节点少的时候,环上的分布可能很不均匀。我曾经遇到过一个场景:3 个节点,其中一个节点扛了 70% 的流量,另外两个闲得发慌。
避坑指南:我曾经在 4 节点集群上只用裸的一致性哈希,结果一个月后某个节点的磁盘直接爆了。后来引入了虚拟节点,每个物理节点在环上映射 100-200 个虚拟位置,分布才均匀起来。
虚拟节点的原理很简单:每个物理节点对应多个虚拟节点,分散在环上。这样即使物理节点少,数据也能均匀分布。实际项目中,我一般建议每个物理节点至少 150 个虚拟节点。
二、范围分片:查询友好,但扩容麻烦
范围分片,说白了就是把数据按某个字段的值范围切分。比如用户 ID 1-10000 在节点 A,10001-20000 在节点 B,依此类推。
这种方案的优点很明显:范围查询效率极高。你想查 ID 从 5000 到 8000 的用户,直接去节点 A 拉数据就行,不需要跨节点聚合。
我在做时序数据库的时候用过这种方案。时间戳天然适合范围分片,查询最近一小时的数据,只需要扫一个分片,速度飞快。
但它的缺点也很致命——扩容时数据迁移量巨大。假设你原来有 4 个节点,每个节点负责 1/4 的范围。现在加一个节点,你得重新划分范围,几乎每个节点都要迁移数据。
| 特性 | 一致性哈希 | 范围分片 | 哈希槽 |
|---|---|---|---|
| 扩容影响范围 | 仅相邻节点 | 全局 | 仅迁移槽 |
| 范围查询 | 差(跨节点) | 优(单节点) | 差(跨节点) |
| 数据均衡 | 需虚拟节点 | 天然均衡 | 手动/自动调整 |
| 实现复杂度 | 中 | 低 | 高 |
还有一个问题:热点数据。如果某个范围的数据访问特别频繁,对应的节点就会成为瓶颈。比如电商大促期间,最近注册的用户数据访问量暴增,负责这个范围的节点就扛不住了。
我的建议:范围分片适合数据访问模式比较均匀、且范围查询是核心场景的系统。如果业务有明显的热点,最好别用。
三、哈希槽:Redis Cluster 的选择
哈希槽方案,最典型的代表就是 Redis Cluster。它把整个哈希空间固定划分为 16384 个槽,每个节点负责一部分槽。数据 key 通过 CRC16 算法计算哈希值,然后对 16384 取模,决定落到哪个槽。
你想想看,16384 个槽,每个槽就是一个数据分片的最小单位。扩容时,只需要把一部分槽从旧节点迁移到新节点,其他槽纹丝不动。
我当年在生产环境部署 Redis Cluster 时,最头疼的就是槽迁移的时机。迁移过程中,源节点和目标节点都要处理请求,网络带宽和 CPU 开销都不小。
关键点:哈希槽方案结合了前两者的优点——扩容影响范围小(只迁移槽),数据分布可控(槽数量固定,可以手动调整)。
但它的实现复杂度也最高。节点之间需要维护槽的分配信息,客户端需要知道每个槽在哪个节点上。Redis Cluster 的做法是:客户端缓存槽映射表,如果发现请求的节点不对,节点会返回 MOVED 重定向,客户端再更新缓存。
这里有个细节:槽迁移期间的请求处理。Redis Cluster 在迁移槽时,源节点会先标记槽为 migrating,目标节点标记为 importing。客户端请求这个槽时,如果 key 还在源节点,直接处理;如果已经迁移走了,源节点返回 ASK 重定向,让客户端去目标节点找。
// 槽迁移的核心流程(伪代码)
1. 目标节点准备:CLUSTER SETSLOT <slot> IMPORTING <node-id>
2. 源节点准备:CLUSTER SETSLOT <slot> MIGRATING <node-id>
3. 批量迁移 key:MIGRATE <host> <port> "" 0 5000 KEYS <key1> <key2> ...
4. 通知所有节点:CLUSTER SETSLOT <slot> NODE <node-id>
这段代码看着简单,实际跑起来坑不少。我曾经遇到过迁移过程中网络抖动,导致部分 key 丢失的情况。后来加了重试机制和校验逻辑,才算稳定下来。
三种方案怎么选?
说实话,没有银弹。我一般这样判断:
- 缓存场景(如 Memcached、自研缓存):一致性哈希 + 虚拟节点,灵活且扩容影响小
- 时序数据(如 InfluxDB、OpenTSDB):范围分片,范围查询是刚需
- 通用 KV 存储(如 Redis Cluster、Codis):哈希槽,均衡性和扩展性兼顾
但实际项目中,往往需要组合使用。比如我做过的一个系统,底层用哈希槽做数据分片,上层用一致性哈希做缓存分片,各取所长。
最后提醒一句:不管选哪种方案,一定要在扩容前做好数据迁移的压测。我曾经在线上直接扩容,结果迁移速度太慢,业务方投诉了半小时。后来学乖了,每次扩容前先跑一遍迁移模拟,估算好时间窗口。
三种方案各有千秋,没有的好坏。关键看你的业务场景:数据访问模式是什么?扩容频率高不高?范围查询是不是刚需?想清楚这些,选型就不难了。