说到 Redis 分片集群,很多人第一反应就是那个神秘的"16384 个散列插槽"。我第一次把它彻底搞明白,是在一次线上扩容事故之后——明明加了节点,热点 key 所在的分片还是被打挂,后来才发现问题根源不是 Redis 本身,而是我没搞懂 slot 的分配和迁移逻辑。这篇文章就把散列插槽这层窗户纸捅破,从它为什么存在、key 如何映射到 slot、集群怎么搭建和迁移,到日常运维里那些坑,一次性讲透。适合正在用 Redis Cluster、或者面试前想把这个机制真正理解到位的人,看完你至少能少踩我当年踩过的那几个坑。
市面上讲 Redis 集群的文章不少,但要么只贴命令,要么只讲概念,很少把"散列插槽"这一个点从原理讲到排障。这玩意既是 Redis Cluster 的基石,也是一堆坑的源头,值得花一整篇的篇幅说清楚。
1. 为什么 Redis 集群偏偏选了"插槽"而不是哈希环
1.1 哈希环与散列插槽的取舍
很多人在接触 Redis Cluster 之前,先见过一致性哈希,比如 Memcached 时代的方案或者某些自研分布式缓存的实现。一致性哈希的典型做法是把所有节点映射到一个 0 到 2^32-1 的环上,key 做 hash 后顺时针寻找第一个节点。好处是节点增减时只有一部分 key 需要迁移,坏处也同样明显:虚拟节点数量设置不合理时数据分布容易倾斜,而且环上没有一个"标准迁移单元",增删节点时需要自己设计数据搬迁策略,复杂度全部丢给业务方。
Redis 选择了另一条路:把整个哈希空间固定切成 16384 份,每一份就是一个散列插槽(slot)。数据进来后,先算出这个 key 属于哪个 slot,再由 slot 的归属规则决定它落在哪个节点。在这里,节点只是"槽位的宿主",slot 才是真正稳定的切分单元。这个设计最大的好处在于:迁移以 slot 为粒度,一个节点挂掉或新增,只需要搬运它负责的那部分 slot,而不是在整张哈希环上做模糊的重哈希。你不需要自己实现环同步、虚拟节点等逻辑,集群内部把这些全包了。
1.2 16384 这个数字不是拍脑袋定的
固定槽位数量为什么是 16384,而不是 65536 或者更多,这是有工程考量的。第一,16384 是 2 的 14 次方,天生适合位运算:key 的 CRC16 结果与 16383 做位与,等价于取低 14 位,比取模快一个量级。第二,集群节点之间通过 cluster bus 互相通信,节点默认每秒会广播自己的状态和负责的 slot 位图。位图大小直接跟 slot 数量挂钩——16384 个 slot 对应大约 2KB 的位图,如果换成 65536,位图就变成 8KB,节点数量一多,心跳和 gossip 消息的体积会明显膨胀,网络开销不可忽视。第三,在实际生产里,单个集群的规模通常不会超过 1000 个节点,16384 个 slot 已经足够做到非常细粒度的负载分配,没必要为不存在的超大规模付出额外的网络成本和内存成本。
1.3 节点与槽位的主从关系
节点启动后加入 cluster,不会自动拥有任何 slot。slot 必须被显式分配,无论是初始化集群时由 redis-cli 自动分配,还是事后用 reshard 手工迁移。这也就是为什么 redis-cli --cluster create 执行后,会看到类似"M: 节点ID 192.168.1.10:6379 slots:[0-5460]"这样的输出。Redis 的期望状态是:每个 slot 有且仅有一个主节点负责,从节点只是主节点的副本,不参与 slot 的直接写入。如果你的集群里出现某些 slot 没有被分配,或者某个主节点手上的 slot 数量跟其他节点差距特别大,那就说明集群处于不健康状态,很多集群异常就是从这种分配不均演变的。
1.4 一个关键认知:slot 计算与数据类型无关
这里要强调一下:slot 计算发生在命令路由之前,与 value 是什么数据类型完全无关。无论你存的是字符串、哈希、列表、集合还是有序集合,进入集群后都是先算 key 的 slot,再决定发给哪个节点。很多人以为只有 string 才会分片,其实 hash、list、set、zset 里的 key 同样会被计算,只是这个动作发生在底层路由阶段,跟 value 结构没有任何关系。理解这一点很重要,后面讲大 key 迁移和热点治理时你会反复用到这个认知。
2. 一个 key 到底落在哪个节点:CRC16 与 hash tag 拆解
2.1 CRC16 的取值过程与 keyslot 验证
Redis 集群算 slot 用的是 CRC16 算法,输出的是一个 16 位校验值,取值范围在 0 到 65535 之间。集群只有 16384 个 slot,所以 Redis 没有直接取模,而是用了位与操作:slot = CRC16(key) & 16383。为什么可以这样?因为 16384 等于 2 的 14 次方,16383 的二进制就是低 14 位全为 1,做位与等价于只保留 CRC16 结果的低 14 位,效果等同于对 16384 取模,但开销更小。
实际验证非常简单,用 redis-cli 直接执行:
redis-cli cluster keyslot user:123它会输出一个 0 到 16383 之间的数字。你随便拿几个 key 试一下就会发现,不管 key 是连续编号还是随机字符串,slot 的分布都比较均匀,不会出现明显的区域聚集。这也是集群能靠 slot 做负载均衡的基本前提——只要分布足够均匀,节点间压力就大体一致。
2.2 hash tag:让多个 key 进到同一个 slot 的规则
Redis 的 hash tag 规则非常简单:key 里如果包含花括号{xxx},slot 计算只针对花括号内的部分。比如 user:{123}:profile 和 user:{123}:orders,花括号里的内容都是 123,两个 key 的 CRC16 结果完全一样,最终落在同一个 slot 里。这样它们就在同一个节点上,MGET、批量更新这类操作就有了执行的可能。
用 hash tag 有几个细节必须注意。第一个细节是多个花括号的处理:Redis 只取第一个"{"之后、第一个"}"之前的内容,说白了就是第一对闭合花括号。第二个细节是空花括号的情况:如果 key 是 user:{}:profile,花括号内是空的,那就当作没有 hash tag,直接用整个 key 计算。第三个细节也是最容易翻车的:如果把用户 ID 这种高基数字段放进了 tag,那么这个用户的所有 key 都集中在一个 slot 里,极端情况下就是数据倾斜和热点打满单节点。hash tag 是双刃剑,它能解决多 key 操作问题,代价是局部集中,使用范围一定要控制住。
2.3 为什么批量命令在集群里经常报 CrossSlot
多 key 命令如 MGET、MSET、DEL、UNLINK,在 Redis Cluster 下面临硬约束:要么所有 key 的 slot 相同,要么命令根本不被允许执行。这是很多刚从单机 Redis 迁移到集群的人第一个踩到的坑:本地开发一切正常,一上集群就报 CrossSlot 错误。要解决只能在业务侧拆分请求——把同一个用户的多个 key 通过 hash tag 聚合到同一个 slot,然后批量操作;或者放弃跨 key 的原子性,改成多次单 key 调用配合逻辑补偿。
这里要注意,pipeline 也有类似问题。管道里如果包含落点不同的 key,发送到节点时会被 MOVED 中断,客户端需要自己处理重放。不同语言的客户端行为还不太一样,有的会自动切换连接,有的直接抛异常,所以迁移到集群前,一定要在性能测试阶段就把这些批量操作场景全跑一遍,别等线上出了问题再排查。
3. 从零搭建分片集群:槽位分配的全过程
3.1 环境准备与节点规划
搭建之前先说环境。很多人问 Redis 怎么下载安装,其实在不同系统上差别不大:macOS 上可以 brew install redis,Windows 上用官方提供的 zip 包或社区编译版本,跑 Linux 服务器就直接源码编译或 yum/apt 安装。用 Docker 部署是现在最省事的方式,官方镜像 redis:7.x 最稳,别用那些来路不明的第三方仓库,之前有人用 docker search redis 时返回 500 错误,多半是本地 docker 引擎或镜像仓库连接的问题,跟 Redis 本身没关系。
我建议至少准备 6 个节点:3 主 3 从。每个主节点负责一部分 slot,从节点负责故障切换。生产环境里节点尽量分散到不同物理机或机架,避免一个机柜断电就把整个集群带走。Docker 部署时特别注意端口映射,除了 6379 这个客户端端口,还有 16379 这个 cluster bus 端口也要暴露出来,否则从节点会连不上主节点,表现为集群虽然创建了,但主从关系总不对。
3.2 初始化集群与自动分配槽位
6 个节点都起来之后,执行:
redis-cli --cluster create \ 192.168.1.10:6379 192.168.1.11:6379 192.168.1.12:6379 \ 192.168.1.13:6379 192.168.1.14:6379 192.168.1.15:6379 \ --cluster-replicas 1redis-cli 会为 3 个主节点均匀分配连续的一段 slot,最后你会看到类似 0-5460、5461-10922、10923-16383 的三段分配。--cluster-replicas 1 表示每个主节点配一个从节点。为什么总节点数喜欢用奇数?因为故障切换时主节点之间要投票选举,奇数的投票机制能避免平票僵局,这也是中间件集群里最常见的经验。
创建完成后,用 cluster nodes 命令可以看到每个节点的 node id、角色、负责的 slot 范围、以及主从关系。cluster info 则会输出 cluster_state:ok、cluster_slots_assigned:16384 这些关键指标。注意 cluster_state:ok 才是健康状态,如果显示 fail,要么有 slot 找不到主节点,要么有主节点失联了。
3.3 手动分配槽位的场景
自动分配适合标准化的三主三从部署,但生产里经常会遇到不一样的需求。比如有的机器配置更高,希望多分一些 slot;或者已有数据的老节点要并入集群。这时候可以先只用 --cluster-replicas 0 建 3 个主节点,后续再加从节点,再手动搬迁 slot。
手动操作时要小心,最忌讳的是以为把 redis.conf 里 cluster-enabled yes 一改、节点一启动就自动进集群了。集群的 slot 分配信息会持久化到 cluster-config-file 指定的文件里,节点重启后会从文件恢复自己负责的 slot。如果这个文件里的节点 ID 和其他节点对上不,就会报类似"Node is not configured"的错误。这个文件一定别删,删了节点就变成一张白纸,所有 slot 关系全丢。
3.4 用命令验证路由和插槽分布
集群搭建完成后,可以用 redis-cli -c -p 6379 连接集群,然后 set foo bar。加了 -c 表示 cluster mode,客户端会自动处理重定向。你会看到类似"Redirected to slot [12182] located at 192.168.1.12:6379"的输出,这就是一次活生生的 slot 路由现场。判断一个客户端工具到底支不支持 Redis 集群,就看它能不能处理这个 Redirected 逻辑。可视化工具方面,新版本的 Redis Insight、Another Redis Desktop Manager 都支持集群模式,能直接看到每个节点的 slot 分布,对排查问题非常方便。但是要提醒一句,工具连接集群时如果走的是普通客户端而非 cluster-aware,频繁跨节点访问时看起来就像"连接时断时续",其实是工具在替你反复做路由流转。
4. 槽位迁移与扩缩容:集群变动的底层逻辑
4.1 reshard 到底在做什么
扩容时最核心的动作就是把一部分 slot 从现有节点搬到新节点,官方命令是 redis-cli --cluster reshard。迁移的最小单位是 slot,但实际搬迁的是这个 slot 下的所有 key。执行时会先问你想迁移多少个 slot、源节点是哪些、目标节点是谁。一旦确认,它就开始逐个 slot 搬移。迁移过程中,源节点和目标节点都会进入特殊状态:源节点对正在迁移的 slot 的写命令会返回 ASK,客户端需要临时把请求转发到目标节点;已经搬过去的 key 会直接从源节点删除,目标节点补齐数据。整个设计很巧妙,既不用停服,也不用全局锁,所有过程对业务都是渐进透明的。
4.2 迁移速度、数据一致性与大 key 风险
默认迁移速度其实比较保守,因为每个 slot 下的 key 是批量传输的,而且转移过程是同步的。想要更快,可以分批操作,每次指定迁移的 slot 数量,比如一次 500 个,分多次做。迁移过程中,源节点不会删除 key,除非确认目标节点已经收到并写入成功。这里有一个非常大的坑:大 key。比如一个几百万成员的 set,或者几十万字段的 hash,在迁移时命令阻塞时间会非常吓人,整个节点的请求都可能卡住。所以上线迁移前,建议先用 redis-cli --bigkeys 扫描一遍,识别大 key,提前拆掉或者规划专门的迁移时间窗口。
4.3 主节点下线与槽位自动接管
集群里如果一台主节点宕机,从节点会发起选举并接管它负责的 slot。整个机制涉及 cluster bus 里的 PFAIL、FAIL 标记和投票晋升。一个从节点要在 master 失联一段时间后,收集到足够多的其他主节点投票,才能把自己提升为新的主节点。很多时候你发现主节点挂了,但业务没有完全中断,就是选举在发挥作用。
正因为这个机制,旧主节点如果事后恢复,它不会拿回 slot,而是自动变成新主节点的从节点。很多人在这里踩坑:旧主恢复后想直接写数据,却报"READONLY You can't write against a read only replica"。这恰恰说明故障切换已经完成了。运维上要养成习惯,节点恢复后先确认角色,再决定是让它继续当从,还是手动切换回来。
4.4 缩容不是把节点删掉那么简单
缩容时直接用 --cluster del-node 删节点,如果这个节点手上还有 slot,Redis 会拒绝操作,提示类似"can't delete, is not empty"。这是很多新手最容易卡住的地方:明明想下线这台机器,却一直删不掉。解决办法很简单,先 reshard 把它的 slot 搬空,再执行 del-node。顺序千万不能反,反了会出现节点反复尝试重连集群,日志不停刷,磁盘和网络都受影响。另外注意,del-node 删的是集群里的节点 ID,容器或进程本身还要自己停掉,两件事不是一回事。
5. 客户端路由的真相:MOVED 与 ASK 到底有什么区别
5.1 支持集群的客户端是怎么工作的
一个支持集群的客户端,启动时会通过 CLUSTER SLOTS 或 CLUSTER SHARDS 拉取整个 slot 到节点的映射表,然后缓存在本地。之后每次命令进来,客户端先本地算 slot,再从缓存表找到目标节点,直接发请求。这就是 smart client 的工作方式。你体感上觉得集群路由"快、稳",本质上就是客户端的映射表够新、够准。很多人用命令行一连觉得集群"重定向很不稳定",大概率是没加 -c 参数,客户端又是普通连接,每次都要等 MOVED 回来再重新发。所以排查集群路由问题,第一件事永远是确认客户端类型和版本。
5.2 MOVED 的永久性与 ASK 的一次性
MOVED 和 ASK 是面试题里最经典的细节,也是实际定位问题必须分清的两种响应。MOVED 代表这个 slot 的主人已经永久变更了,客户端收到 MOVED 后,应该更新本地缓存的映射关系,后续请求直接发往新节点。而 ASK 出现在迁移过程中,它表示这个 slot 正在搬,源节点可能有部分 key 已经不在了,让你去目标节点那边碰运气。但迁移完成之前,目标节点对没有迁过来的 key 会拒绝,所以在请求前需要先发一条 ASKING 命令,告诉目标节点"放行这一次对该 slot 的访问",注意只是这一条请求,下次访问还要重新发 ASKING。说白了,MOVED 是永久通知,ASK 是临时许可,两者处理逻辑完全不同。
5.3 分布式锁与跨 slot 场景下的设计
很多人会在集群上用 Redis 做分布式锁,比如 Redisson。锁 key 经过 slot 计算后会落到某个具体节点,这在单锁场景下没有问题。但如果你在锁里还带着业务数据,或者锁续期的逻辑里还要去读其他 key,就很容易踩跨 slot 的坑。我见过一个线上事故:代码里用 Redisson 加锁,锁 key 是 order:lock,业务 key 却是订单号和用户 ID 拼接的,结果业务 key 和锁 key 分布在不同的 slot,Lua 脚本执行不了,最后只能改成分段锁绕道,排查了很久。正确的做法是把锁 key 和相关的业务 key 都放进同一个 hash tag,比如 order:{123}:lock 和 order:{123}:detail,让它们落在同一个 slot,Lua 脚本才能原子执行。
5.4 缓存穿透治理与 slot 负载的关系
网上聊缓存穿透,大多围绕布隆过滤器和空值缓存。但在集群环境里,缓存穿透还有一个隐藏问题:如果热点 key 被固定在某一个 slot,所有请求都汇聚到同一个节点,就算集群有几十个节点也分担不了。排查时可以看各节点的瞬时 QPS 差距,如果某个节点 CPU 明显高于其他节点,八成是某个热 key 的 slot 被打满了。治理思路无非几种:在业务层把热 key 拆成多个带后缀的副本 key,比如 sku:{10001}:data 拆成 sku:{10001}:0 到 sku:{10001}:31,再按用户请求特征分发;或者用本地缓存加随机过期时间;再或者用读写分离把读流量分到从节点。理解了 slot 分布的原理之后,再看这些治理方案,思路会清晰很多。
6. 生产环境中的故障排查与避坑实录
6.1 slot 相关常见错误速查表
先整理一张速查表,方便大家日常对照。
| 错误信息 | 含义 | 处理思路 |
|---|---|---|
| CLUSTERDOWN The cluster is down | 部分 slot 找不到可用的主节点,或集群状态异常 | 检查 cluster nodes 里 slot 归属,确认无节点失联 |
| MOVED | 客户端访问了旧节点,slot 已永久迁移到新节点 | 更新客户端本地映射,直接发给新节点 |
| ASK | slot 正在迁移,源节点提示去目标节点重试 | 先发 ASKING,再在目标节点执行该请求 |
| CrossSlot | 批量操作的多个 key 不在同一个 slot | 用 hash tag 聚合 key,或拆分请求 |
| ERR Slot ... already busy | 执行集群分配命令时槽位已有归属 | 检查 slot 占用情况,确认分配范围不冲突 |
| READONLY | 写命令发到了从节点 | 确认节点角色,读写请求都要走 master |
在实际排查时,我会先执行 redis-cli --cluster check 加上任意一个节点的地址,它会自动检查整个集群的健康度、slot 覆盖情况和主从关系,基本一眼就能看出 slot 有没有缺、节点是否可达。这是我每次动完集群之后必跑的一步。
6.2 一个真实数据倾斜案例的排查过程
拿我之前做的一个案例来演示。集群 6 个节点,本来请求分布很均匀,有一阵子某个分片突然 CPU 跑满。第一步,先看 cluster nodes 确认 slot 分布,发现三个主节点的 slot 数量都正常。第二步,用 redis-cli --hotkeys 配合 allkeys-lfu 策略临时扫描热点 key,发现是某个活动 SKU 的维度 key,因为用户都点这个商品,这个 key 所在的 slot 压力巨大。第三步,我们把大 key 拆成 32 个副本 key,sku:10001:part:0 到 part:31,并在业务层按用户 ID 的 hash 落到不同的副本 key 上。改造后,原来打满单节点的流量被分散到了 32 个 slot 覆盖的多个节点,负载瞬间就平了。这个案例给我的教训是:遇到单节点热点,不要第一反应加机器,先看是不是热 key 集中在单 slot。
6.3 迁移期间的超时、慢日志和持久化问题
Redis command timed out 这类报错在集群迁移期间特别常见,尤其是用了 Lettuce 的客户端,因为 Lettuce 默认的 command timeout 比较短,如果正好赶上大 key 迁移或者节点阻塞,线程池里的请求就会大面积超时。我们的应对方案分三层:第一,提前拆大 key,不让大 key 进入迁移链路,这是从源头上解决;第二,客户端把 command timeout 调整到合理范围,比如 3 到 5 秒,留出重试余量;第三,借助 redis 日志和 SLOWLOG 查找耗时命令,确认是不是有慢操作把节点拖住了。迁移期间还要关注持久化,因为 RDB 快照和 AOF 重写都是吃磁盘 IO 的,迁移本身也有网络和磁盘开销,如果正好撞上持久化任务,超时概率会成倍增加,建议把迁移窗口和持久化任务错开。
6.4 可视化工具、常用命令和运维习惯
日常运维我习惯用 Another Redis Desktop Manager 或 Redis Insight 连集群。这里有一个小细节:如果工具只支持单节点连接,你用普通模式连集群虽然也能连上,但每次访问不同 key 都可能跨节点跳转,看起来像是"连接不稳定",其实是工具没有走 smart client 的映射逻辑,每次路由都要重查。真要看 slot 分布,直接在命令行执行 cluster nodes,一行一个节点,每个节点负责哪些 slot 段清清楚楚,比看任何图形界面都直观。
还有一个运维习惯值得分享:每次变更集群结构,无论是扩容、缩容、还是迁移 slot,操作完后随手跑一次 redis-cli --cluster check。别嫌重复,再老的司机也有手抖的时候,集群的可用性完全靠这个兜底。我甚至在 CI 脚本里都放了这个检查步骤,每轮发布后自动执行,有问题能第一时间暴露出来。
最后再分享一个经验:玩明白散列插槽,等于把 Redis Cluster 的命脉握在手上了。无论是面试问 MOVED 和 ASK 的区别,还是线上排查热点倾斜,本质上都是在对 slot 的理解做检验。我自己的习惯是把每个集群的 slot 分布、节点角色、大 key 清单都记在一张表里,每次出问题先查表再定位,能省一半的排查时间。希望这篇能把你的思路也理顺,少走点弯路。