数据分片原理:一致性哈希、范围分片、哈希槽

分片这件事,说白了就是把数据打散,分散到不同的节点上。但怎么打散,打散后怎么找回来,这里面的门道可不少。我这些年经手过的分布式系统,从 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):哈希槽,均衡性和扩展性兼顾

但实际项目中,往往需要组合使用。比如我做过的一个系统,底层用哈希槽做数据分片,上层用一致性哈希做缓存分片,各取所长。

最后提醒一句:不管选哪种方案,一定要在扩容前做好数据迁移的压测。我曾经在线上直接扩容,结果迁移速度太慢,业务方投诉了半小时。后来学乖了,每次扩容前先跑一遍迁移模拟,估算好时间窗口。

三种分片方式对比 一致性哈希 • 哈希环结构 • 增删节点影响小 • 需虚拟节点防偏斜 • 范围查询效率低 • 代表:Cassandra 范围分片 • 按值范围切分 • 范围查询极快 • 扩容迁移量大 • 热点数据问题 • 代表:Bigtable 哈希槽 • 固定槽数量 • 扩容只迁移槽 • 数据分布可控 • 实现复杂度高 • 代表:Redis Cluster 适用场景 • 缓存集群 • 负载均衡 • 动态扩缩容 • 节点数量变化频繁 适用场景 • 时序数据库 • 日志存储 • 范围查询为主 • 数据访问均匀 适用场景 • KV 存储 • 内存数据库 • 需要均衡分布 • 固定集群规模

三种方案各有千秋,没有的好坏。关键看你的业务场景:数据访问模式是什么?扩容频率高不高?范围查询是不是刚需?想清楚这些,选型就不难了。