
说到 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 redisWindows 上用官方提供的 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 cant write against a read only replica。这恰恰说明故障切换已经完成了。运维上要养成习惯节点恢复后先确认角色再决定是让它继续当从还是手动切换回来。4.4 缩容不是把节点删掉那么简单缩容时直接用 --cluster del-node 删节点如果这个节点手上还有 slotRedis 会拒绝操作提示类似cant 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 分布在不同的 slotLua 脚本执行不了最后只能改成分段锁绕道排查了很久。正确的做法是把锁 key 和相关的业务 key 都放进同一个 hash tag比如 order:{123}:lock 和 order:{123}:detail让它们落在同一个 slotLua 脚本才能原子执行。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 已永久迁移到新节点更新客户端本地映射直接发给新节点ASKslot 正在迁移源节点提示去目标节点重试先发 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 个副本 keysku: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 清单都记在一张表里每次出问题先查表再定位能省一半的排查时间。希望这篇能把你的思路也理顺少走点弯路。