
聊到 Redis 集群很多人第一反应是“不就是把几台 Redis 连起来嘛”可真到线上环境里踩过坑你会发现事情远没那么简单。我前几年给一个电商项目做过一次 Redis 扩容当时就栽在“以为集群只是多开几个实例”上数据分布不均匀、故障转移后丢写、跨 key 操作直接报错……一晚上连滚带爬才把线上稳住。今天这篇就把 Redis 集群的核心原理一次讲透从数据分片到故障转移、槽位迁移到客户端路由该说的参数和坑一个都不落下。适合刚接触集群的后端开发、被集群高可用问题折磨的运维以及准备 Redis 面试的人照着这些原理去排查线上问题会比死记命令高效得多。1. 为什么要做集群单机 Redis 的瓶颈与三种演进路线1.1 单机版能扛多大压力瓶颈到底在哪儿Redis 单机版在高并发场景下表现并不差。读请求轻松上 10 万 QPS写请求也能到几万级别配合上 Pipelining 和连接池优化大部分中小业务单机完全够用。但问题出在三个地方。内存容量。一台物理机的内存是有限的就算你上了 512GB 的内存条数据量继续增长怎么办Redis 最擅长的是把热点数据放在内存里可一旦数据集超过单机内存就只能加机器、换大内存服务器成本曲线非常陡。CPU 瓶颈。Redis 主线程是单线程模型一条命令从解析协议、查哈希表、执行操作到返回结果全在一个线程里跑。命令处理路径上几乎没有并行就算多核服务器也只吃满一个核。你可能说现在有 Redis 的多线程 IO但那只是处理网络读写和协议解析真正执行命令还是串行的。当业务 QPS 冲到几百万单核产能就是硬上限。单点故障。这是最致命的。单机 Redis 挂了缓存直接打穿数据库立刻承接所有流量八成会被压垮。哪怕是做了 AOF 和 RDB 持久化恢复也要时间在这期间服务就是不可用的。所以生产环境至少要上主从复制让从节点随时能顶上来。1.2 从主从复制到哨兵再到 Cluster每个模式解决了什么问题Redis 的集群能力不是一步到位的它经历了三个阶段。最开始是主从复制结构上是一主多从。主节点负责写从节点负责读和备份。这套方案解决了两个问题一是数据有冗余主节点挂了可以通过人工操作把从节点提升为主节点二是读写分离把读流量分散到多个从节点上。但它的短板很明显——故障切换得靠人。半夜三点主节点挂了需要运维起来手动执行 SLAVEOF NO ONE这个恢复时间可能就是事故级别。然后是哨兵模式。哨兵Sentinel负责监控主从节点的健康状态一旦主节点失联哨兵会自动选出一个从节点提升为主整个过程秒级完成应用通过哨兵感知新的主节点。哨兵模式把高可用做到了自动化但它依然没有解决“单机写能力”和“数据容量”的问题。写还是只能写在一个主节点上内存容量也还是受单机限制。最后是 Redis Cluster这是官方在 3.0 版本推出的分布式解决方案。它把数据自动分片到多个主节点上每个主节点负责一部分数据写能力可以水平扩展内存容量也能跟着节点数线性增长。与此同时集群内部还内置了类似哨兵的高可用机制主节点挂了会自动将从节点提升上来。这才是真正意义上的“集群”。这里有个常被混淆的点Redis Cluster 和“哨兵 主从”不是替代关系而是解决不同问题的方案。如果业务只有几 GB 数据、几万 QPS哨兵模式足够了如果数据到了几百 GB、写流量单机扛不住才需要考虑 Cluster。还有一种折中做法是使用代理中间件比如 Codis、Twemproxy做分片它们把路由逻辑放到了代理层客户端无感知。这套方案在业界也有不少历史存量但现在新项目再搞官方 Cluster 基本是更省心的选择。2. 数据分片原理16384 个哈希槽是怎么把 key 分布到节点的2.1 从 hash 取模到一致性哈希为什么 Redis 选择了哈希槽做分布式存储第一步就是回答“数据放在哪”。最简单的方案是 hash(key) % NN 是节点数。这个方案的痛点是一旦节点数变化大部分 key 的映射关系都会变需要大规模迁移数据。为了解决这个问题人们发明了一致性哈希让每个节点在哈希环上占一段扩容缩容只影响附近的一小片数据。但一致性哈希也有自己的问题——它在节点不均衡时需要引入虚拟节点来打散数据管理起来比较绕。Redis Cluster 用的方案是“哈希槽”hash slot思路其实更直接。整个 Redis 键空间被固定分成 16384 个槽每个 key 通过 CRC16 算法算出一个 16 位的结果再对 16384 取模得到一个 0 到 16383 的槽编号。槽再均匀地分配给集群里的主节点。比如 3 个主节点每个节点分管约 5461 个槽。slot CRC16(key) 16383与一致性哈希相比哈希槽方案的好处是边界非常清晰。槽是一个固定的离散集合扩容缩容时只需要把槽整体搬走数据迁移的范围完全可控。集群内每个节点都保存着“槽 → 节点”的映射表客户端可以通过命令获取这张表从而精确知道某个 key 应该在哪个节点上。这里有一个面试高频题为什么是 16384而不是 65536原因有几层。第一节点间心跳包需要携带节点自身维护的槽位位图如果槽位太多位图体积会变大。16384 个 bit 是 2KB65536 个 bit 是 8KB节点数量多了之后每个节点每秒钟都在互相发送心跳这部分带宽浪费不容小觑。第二集群节点的规模一般到几百个就到顶了16384 个槽完全足够分配再设更多只会增加元数据维护成本。第三槽的数量越少reshard 时的迁移粒度就越粗但 16384 个槽已经能把数据粒度控制在一个相对合理的范围既能均衡分布又不至于在迁移时产生过大的元数据开销。2.2 一个 key 的完整“寻址”过程与 hash tag 的妙用很多同学刚接触集群时会有个疑问客户端到底是怎么知道该把命令发给哪台机器的实际上在 Redis 5.x 之后的版本中客户端比如 Jedis、Lettuce、Redisson会通过 CLUSTER SLOTS 命令获取槽位和节点的映射关系并把这个映射缓存在本地。发命令时客户端先计算 key 的槽位然后根据本地缓存找到对应的节点直接把请求发过去。如果本地缓存是陈旧的呢节点收到命令后发现槽位不在自己这里会返回一个 MOVED 错误客户端收到 MOVED 后会更新本地缓存并重新发送请求。槽位计算方式本身不复杂但实际开发中有一个非常实用的技巧用 hash tag 来控制 key 的分布。在 key 的任意位置用花括号包住一部分字符串Redis 在计算槽位时只针对花括号内的内容做 CRC16。比如{user:1001}.profile和{user:1001}.orders因为花括号里的内容相同它们会落进同一个槽。这在需要批量操作多个 key 的场景下非常关键。Cluster 模式下MULTI 事务、Lua 脚本、MGET、MSET 这类命令都要求所有操作的 key 必须在同一个槽里。如果没有 hash tag两个普通 key 大概率会被分到不同槽命令直接报 CROSSSLOT。把相关业务 key 放在同一个 tag 里是集群模式开发绕不开的基本功。但要注意hash tag 用多了会产生数据倾斜比如某个超高热度的用户被 tag 到同一个槽槽内流量可能成为瓶颈。生产中要根据实际业务控制 tag 的粒度别把一个用户的所有 key 都塞进一个 tag 里那和把所有鸡蛋放一个篮子没区别。3. 集群内部通信Gossip 协议与节点间的“朋友圈”3.1 心跳消息里到底装了什么Redis Cluster 不是纯客户端路由方案节点之间需要互相通信才能维护集群状态。节点间通信走的是一个独立的端口通常叫 cluster bus 端口默认是客户端端口 10000。比如 Redis 监听 6379那 cluster bus 就监听 16379。这个端口用于节点之间的 PING、PONG、MEET、FAIL 等消息传递。节点间通信采用的是 Gossip 协议。你可以把它理解成“朋友圈里的消息扩散”每个节点每隔一段时间随机找几个节点聊两句交换彼此知道的信息。每个节点都会向其他节点发送 PING 消息其他节点收到后回复 PONG。这些消息里除了常规的心跳时间戳还要携带发送节点的状态信息包括节点 ID、角色主/从、槽位信息、复制偏移量以及每个节点维护的集群状态视图。槽位信息的传递不是每次全量发送所有槽位清单而是通过一个长度为 16384 bit 的位图来表示。这个位图的每一位代表一个槽置 1 表示这个槽属于发送节点。这样可以极大压缩消息体积。但即便如此节点数量多了以后PING/PONG 消息里的 gossip 段会包含多个节点的状态信息消息整体变长网络开销就跟上来了。另外Gossip 协议天然有“传播延迟”。一个节点发现另一个节点下线这个消息不会瞬间同步到集群所有节点而是像滚雪球一样慢慢扩散。这也是为什么 Redis 集群的故障检测需要一定时间从几十毫秒到十几秒不等具体取决于 cluster-node-timeout 的配置。3.2 为什么说集群节点数量不能无脑加经常有人问“Redis Cluster 是不是节点越多越好”从数据容量和写性能来看节点越多确实越能扛。但从内部通信的角度看节点数量增加会带来两个直接后果。第一是带宽压力。每个节点默认每秒会 PING 一部分随机节点消息里还要携带 gossip 信息。节点数从 10 涨到 100每个节点的心跳消息携带的节点状态、位图信息都会变大集群内部的消息总量是呈平方级增长的。很多集群节点数量到了几百个之后会发现集群带宽被内部通信吃掉一大块业务流量反而受影响。官方给出的建议是集群规模控制在 1000 节点以内实际生产里我见过很多团队把规模压在 300 节点以下管理成本和故障半径都更容易控制。第二是故障检测变慢。Gossip 是概率性的传播节点越多一条消息传遍全网的时间越长。如果一个节点故障其他节点需要依赖各自的超时判断和消息扩散才能达成共识。节点少的时候可能一两秒就能完成故障转移节点多了之后可能要等更久因为信息还在传播路上。所以在设计集群规模时不要贪多。遇到容量瓶颈先考虑大内存机型再考虑拆分业务集群最后才是无限堆节点。这个顺序我用下来基本是成本最低的。4. 请求路由与 MOVED/ASK一条命令是怎么找到正确节点的4.1 三种集群客户端连接方式与 MOVED 重定向客户端连接 Redis Cluster 有三种常见姿势。第一种是直连模式客户端通过 CLUSTER SLOTS 拉取槽位映射之后按需直连目标节点。这也是官方推荐的方式性能最好没有中间层转发损耗。第二种是代理模式客户端只连代理节点代理负责转发请求和路由逻辑常见的有 Redis Cluster Proxy 这类的组件。这种方式客户端接入简单但多一跳网络延迟架构里也多了一个潜在瓶颈点。第三种最简单粗暴客户端不缓存槽位映射每次都随机连一个节点收到错误的响应后再把请求重新发到正确节点。Redis 原生的 redis-cli 在集群模式下走的就是这条路它会根据服务端返回的 MOVED/ASK 自动重试。这里最容易被忽略的是 MOVED 和 ASK 的区别。当客户端请求的 key 对应的槽已经不在当前节点上时当前节点会返回 MOVED并附上正确的节点 IP 和端口。这个响应意味着槽位的归属已经永久改变客户端收到 MOVED 后必须更新本地槽位缓存再把请求转发到新节点。如果客户端没有更新缓存它每一条命令都会先打到一个错误节点再被重定向一次性能会非常难看。在旧版本的 Jedis 中比如 2.8.x如果开启了集群模式拿到 MOVED 后会自动处理但如果你用的是普通单机连接它是不会自动跟随重定向的。这个细节曾经坑过很多人排查半天发现数据没问题就是每个请求多了一次网络往返。4.2 ASK 重定向与槽迁移期间的微妙状态MOVED 处理的是槽位已经永久迁移的场景。那么在数据迁移的过程中呢假设我们把槽 100 从节点 A 迁到节点 B迁移不是瞬间完成的有一个中间态源节点 A 把槽 100 标记为 MIGRATING正迁出目标节点 B 把槽 100 标记为 IMPORTING导入中。在这个窗口期如果客户端请求的 key 还在 A 上A 直接返回数据如果 key 已经被迁移到 BA 这个时候就不会返回 MOVED而是返回 ASK。ASK 的含义是“这个槽目前正在迁移你要找的 key 已经搬走了请先去 B 节点发一个 ASKING 命令然后再执行你的请求。”为什么需要多一个 ASKING 命令因为正常来说B 节点还不认为槽 100 属于自己如果有客户端直接把请求发过来B 会返回 MOVED让客户端去找 A。但 ASKING 命令相当于一个临时通行证B 收到 ASKING 后允许客户端访问正在导入中的槽。换句话说MOVED 是“永久搬迁”的通知ASK 是“临时转移”的通知。客户端收到 MOVED 要更新本地缓存收到 ASK 只需要在这一次请求里做一次特殊处理不需要修改缓存。这个细节在面试里经常被拿出来考因为很多人只记住了“重定向”三个字却分不清重定向的类型。5. 高可用机制故障发现、主观下线与客观下线5.1 故障检测的两个阶段与 PFAIL/FAIL 标记Redis Cluster 的高可用是自动的但它不是某一台中心节点说了算而是靠所有节点集体“投票裁定”。这个检测过程分两个阶段对应两个标记状态主观下线PFAIL和客观下线FAIL。主观下线是单节点视角。节点 A 如果持续向节点 B 发送 PING但 B 一直没有回复超过 cluster-node-timeout默认 15000 毫秒A 就会把 B 标记为主观下线。注意这只是 A 自己认为 B 可能挂了并不代表集群的最终判断。客观下线是集群视角。当某个节点发现一个持有槽位的主节点被标记为主观下线后它会把这个信息通过 Gossip 消息传播出去其他节点收到后会发现“哦原来 B 也挂了”。当持有槽位的主节点中数量超过一半的节点都认为 B 挂了那么 B 就会被标记为客观下线触发后续的故障转移流程。这里有个容易误解的点不是所有节点下线都会触发故障转移。只有“持有槽位的主节点”下线才需要重新选主。如果只是一个从节点挂了或者一个没有槽位分配的主节点挂了集群只会把它的状态标记为 FAIL但不会进行主从切换因为数据本身没有丢失风险。所以我们在排查故障时要先确认挂掉的是主节点还是从节点是持有槽位的主节点还是普通节点。5.2 从节点晋升带 Raft 思想的选举过程故障转移的核心是从节点晋升。当一个持有槽位的主节点被标记为客观下线后它的从节点会发起选举。这个选举机制借鉴了 Raft 算法的思想核心原则有两条谁的数据新谁优先获得大多数持有槽位主节点的投票才能胜出。具体来说从节点在确认主节点客观下线后不是立刻发起选举而是先等待一段时间。这个延迟时间是为了给数据同步留窗口同时也防止主节点只是网络抖动、很快恢复的情况。延迟长短跟当前 epoch纪元和复制偏移量有关简单理解复制偏移量越大的从节点数据越接近原主节点被选中的优先级越高。选举时每个持有槽位的主节点都拥有投票权。从节点向这些主节点发送投票请求收到超过半数票数之后从节点晋升为新主节点执行 SLAVEOF NO ONE并把原本由原主节点负责的槽位接管过来。这个过程结束后新主节点会广播 PONG 消息让集群中的其他节点更新槽位映射。这一整套流程看似顺利但在实际生产中故障转移经常引发另一类问题数据丢失。因为 Redis 主从复制默认是异步的主节点收到写命令并返回成功不代表从节点也同步到了这份数据。原主节点故障时总有一部分最新写入的数据还没复制到从节点新主节点晋升后这部分数据就丢了。这里要意识到 Redis Cluster 保证的是“最终可用”和“尽量不丢”而不是强一致。如果业务要求严格不丢写单靠集群默认配置是不够的需要结合 WAIT 命令、min-replicas 参数或者引入其他同步机制。6. 集群伸缩与数据迁移扩容/缩容时数据怎么搬6.1 在线扩缩容的核心机制集群上线后随着业务增长扩容是免不了的事。Redis Cluster 支持在线扩容过程中服务不需要停止。核心操作就是槽迁移官方工具是 redis-cli --cluster reshard。执行 reshard 时需要指定目标节点谁接收槽、迁移多少槽、源节点从谁那里迁出。工具内部会做几件事把源节点上的目标槽标记为 MIGRATING把目标节点上的目标槽标记为 IMPORTING然后逐个把槽内的 key 从源节点搬到目标节点。每个 key 的迁移走的是 MIGRATE 命令源节点把 key 序列化后发给目标节点目标节点收到并写入成功后再通知源节点删除本地 key。这里有一个值得注意的点迁移是按槽、按 key 逐个推进的不是把整个槽一次性复制过去。如果某个槽里面塞了几百万个 key迁移过程会持续挺长时间期间这些 key 的访问可能经历 ASK 重定向延迟会有所上升。所以在做 reshard 之前最好关注一下目标槽的 key 数量历史最坏情况下迁移一个槽可能要跑好几个小时。为了避免影响线上体验我会选在业务低峰期操作并先把集群带宽余量留出来。另外扩容一个主节点不是简单地把新实例加到集群里。需要先通过 CLUSTER MEET 让新节点加入集群再用 CLUSTER ADDSLOTS 给它分配槽位或者用 reshard 从老节点迁槽过去如果这个新节点还要带从节点也需要通过 redis-cli --cluster add-node 指定 --cluster-slave 参数来挂载。整个操作链条比单机版本复杂得多强烈建议先在测试环境完整演练一遍形成自己的操作手册再上生产。6.2 迁移过程中客户端会经历什么槽迁移过程中客户端并不需要感知整个流程的细节它只需要正确处理两个重定向MOVED 和 ASK。具体展开一下。客户端访问一个正在迁移的 key 时如果 key 还在源节点源节点直接返回结果如果 key 已经被搬走源节点返回 ASK并带上目标节点的地址。客户端需要先向目标节点发送 ASKING再重发原来的命令拿到结果。因为 ASK 是一次性的客户端不会因为它而修改槽位缓存所以下一个 key 再请求时还是会先问源节点然后看情况再次 ASK。如果整个槽的迁移都完成之后槽的归属就正式变更了。这时源节点会收到 CLUSTER SETSLOT NODE 命令把槽的归属权交出去。这个操作需要向集群内的所有主节点广播确保大家统一认识否则客户端拿着旧映射一访问就会收到 MOVED又绕一次。迁移期间的性能问题也很值得关注。我自己遇到过的情况是迁移的单 key 过多时源节点和目标节点之间的网络连接频繁建立导致节点 CPU 飙升、命令处理延迟变大。后来优化办法是批量 MIGRATERedis 支持一次迁移多个 keyMIGRATE ... KEYS key1 key2 key3减少往返次数。官方 reshard 工具内部也会尽量批量处理但如果我们手动写脚本迁移务必用 KEYS 参数做批量不要循环一条一条 MIGRATE否则性能会非常难看。7. 集群模式下的经典问题与踩坑实录7.1 跨槽操作失灵、分布式锁失效与常见坑集群模式和单机版在功能上有一个重要差异单机版的很多命令是“全局”的但集群模式必须考虑槽位。最常见的报错是 CROSSSLOT Keys in request dont hash to the same slot。只要是涉及多 key 的命令MGET、MSET、DEL 多个 key、MULTI 事务、Lua 脚本都要求所有 key 在同一个槽否则直接拒绝执行。解决办法就是用 hash tag把业务强关联的 key 放到同一个{}中。但也别高兴太早hash tag 用多了会出现“热点槽”效应。比如某个大商家的几百个 key 都挂在同一个 tag 下那么检索、更新这些 key 的请求全部压到同一个主节点上。这个主节点的负载就会异常高而其他节点闲着。所以在设计 tag 时要尽量均匀分布或者把热 key 做拆分让 hot key 分散到不同槽。另一个高频话题是分布式锁。Redis 官方推荐的分布式锁方案是 SET key value NX PX 过期时间这个命令在集群模式下同样可以用因为它只操作一个 key。但它有一个隐患如果主节点加锁成功后立刻故障锁还没复制到从节点然后新的主节点被选出来锁就“凭空消失”了。另一个客户端可能也拿到锁导致两个客户端同时认为自己持有锁。这个场景虽然概率很小但属于分布式系统的经典逻辑问题。要解决需要引入 RedLock 算法或者使用 ZooKeeper 这类带强一致语义的协调服务。我的建议是对锁安全性要求极高的业务优先用专业协调组件一般业务用 Redis 做锁可以接受但要清楚它的边界在哪里。7.2 故障转移、槽迁移中的隐蔽问题排查速查平时在群里帮人看问题发现很多集群故障都是几个老面孔反复出现。我把它们整理成一个排查表大家以后遇到类似现象可以直接对号入座。现象可能原因排查方式与解决思路客户端报 CLUSTERDOWN The cluster is down集群中部分主节点认为客观下线且没有可用从节点完成切换或者槽位缺失比如某个槽没有主节点负责CLUSTER INFO 检查 cluster_state确认是否有槽位没有主节点恢复故障节点或补从节点故障转移后部分数据丢失异步复制导致主节点故障前未同步到从节点的写入全部丢失写入侧用 WAIT、集群主节点配置 min-replicas-to-write 与 min-replicas-max-lag降低丢失风险新节点加不上集群一直处于握手状态cluster bus 端口客户端端口 10000未放通或 CLUSTER MEET 的 IP 不通检查防火墙和云安全组直接用 telnet 测试集群端口之间的连通性槽迁移期间大量超时迁移单 key 过多、网络往返频繁或者一个槽内 key 数量巨大批量 MIGRATE选择低峰期迁移必要时可以把超大槽拆到多个节点分头迁网络抖动导致主节点被误判下线触发无谓切换cluster-node-timeout 设置过短或者宿主机负载过高导致心跳延迟调大 cluster-node-timeout给 Redis 节点预留足够 CPU检查宿主机网络与 CPU 抢占情况两个主节点短时间内频繁切换网络不稳定内存 swap 或 GC 停顿导致节点无响应排查节点内存使用和 swap关闭 THP调整 vm.swappiness扩容内存或升级机型集群内所有节点之间带宽消耗高节点数过多gossip 心跳消息体变大控制集群规模或在心跳开销大的场景下把节点拆分为多个独立集群隔离互不影响这张表里的很多问题其实都可以通过“提前预防”来规避。比如部署时把 cluster-node-timeout 从默认的 15000 调到一个更贴合网络环境的数值太小了容易误判太大了故障转移慢再比如所有节点预留 20% 的内存余量避免内存写满触发 OOM。还有给 Redis 配置好系统层面的内存和 TCP 参数比如禁用 THP、设置合适的 backlog这些细节在其他教程里很少会提到但我在生产环境里遇到过几次影响极大。7.3 集群日常巡检与核心监控项集群部署完成不代表万事大吉日常巡检很重要。我常用的检查命令有三个CLUSTER INFO 看集群整体状态CLUSTER NODES 看节点角色和 PFAIL/FAIL 标记redis-cli --cluster check 可以快速检查槽位分配是否完整、有没有孤儿节点。监控层面重点关注几个指标每个节点的内存使用率、命令处理耗时、网络入口出口流量、主从复制偏移量差值以及客户端连接数。复制偏移量差值是最容易忽视的。如果主从偏移量长期拉大说明从节点跟不上主节点的写入速度一旦主节点故障从节点提升后会丢失大量数据。所以监控里看到主从偏移量差值持续增长要立刻处理常见原因是从节点所在宿主机磁盘性能差或者网络延迟高。另一个建议是定期做“故障演练”。我每半年会找一个业务低峰期手动把某个主节点 kill 掉观察集群是否自动完成了切换、客户端有没有感知、恢复时间是多少。演练几次后你会发现很多潜在问题第一次就暴露了比如某个从节点数据延迟过大、某些客户端连接策略不合理。等真出事故时团队已经有肌肉记忆了不会手忙脚乱。8. 写在最后的一点实操体会如果让我给刚接触 Redis Cluster 的人一句建议那就是不要只背原理一定要自己动手搭一个三主三从的最小集群亲手触发一次故障转移和槽迁移。我自己的经历是第一次在生产环境扩容时因为没提前演练 reshard 流程操作到一半发现槽迁移太慢只能硬着头皮回滚那一次之后我才真正理解了 MIGRATING 和 IMPORTING 状态的含义。还有一个小技巧排查集群问题时先把 epoch 看清楚。epoch 相当于集群配置的版本号主从切换会让 epoch 递增。如果两个主节点同时对外服务多半是配置版本不一致或者网络分区导致的“双主”假象这时候看 epoch 能快速定位谁才是真正的新主节点。最后再强调一句Redis Cluster 是一个高效但并简单的东西很多线上事故都源于对它的“想当然”。希望这篇原理拆解能帮你少踩一些坑把这些机制吃透后面不管遇到什么问题都能从容应对。