想象一下,你是城市里一家快递公司的调度主管。一开始,公司在老城区只有一个小仓库,所有包裹都堆在这里。你只需要一个青册子,把快递单号记下来,就能知道包裹存哪儿、该往哪儿送。这就是单机 Redis——简单高效,库存全在一个本本上。 可是城市越来越大,双十一来了!单日包裹从几万件暴增到几千万件。小仓库的货架被塞满,青册子厚得像砖头,几个老员工忙得脚不沾地、排队取件的客户快要打起来了。单机 Redis 的 CPU 冲到 100%,内存被打爆,请求大面积超时——你被老板一顿臭骂。 冷静下来,你琢磨出一个点子:与其把所有包裹塞进一个仓库,不如在城市里开七家分拨中心,每个分拨中心负责一个区。可是问题来了:一件包裹该发往哪个分拨中心?分拨中心之间怎么打个招呼确认“货在谁家”?万一某个分拨中心突然着火了,积压的包裹怎么办?客户怎么知道自己的包裹该去哪个分拨中心取? ——这就是 Redis Cluster 要解决的核心命题。 Redis Cluster 的答案是:把整个仓库划分成 16384 个“抽屉”(哈希槽),每个抽屉贴上一个编号;分拨中心(节点)之间靠“员工小纸条”(Gossip 协议)互相传递消息,盘点各自的抽屉;某家分拨中心失火后,大家开会重新指派负责人(故障转移)。而客户拿到一张“抽屉归属表”,一查就能直奔正确的分拨中心。这套机制,让 Redis 从“单打独斗”变成了“千军万马”。 这篇文章,我会带你从零搭建这套“快递分拨网络”。全程不讲艰深术语,只用快递公司的日常运维场景,把集群的每一颗螺丝都拧给你看。
对后端架构与缓存工程师而言,理解 Redis Cluster 是走向高并发分布式系统设计的必修课——它几乎是缓存层支撑海量流量的唯一标准方案。
可视化演示
展示Redis Cluster的16384哈希槽分片机制、Gossip协议节点通信与故障检测、客户端MOVED重定向路由的完整集群架构。
本节配有交互动效(进入视口后自动加载,约 3KB)。
哈希槽分片机制 16384个哈希槽均分到4个节点,键经CRC16哈希定位槽位 每个节点负责一段连续槽位范围,扩缩容只需迁移槽位 slot = CRC16(key) % 16384 哈希槽分片避免全局一致性哈希复杂性
单机瓶颈
快递公司的故事从老城区的小仓库开始。仓库虽好,但有一个硬伤:扩容只能靠“堆人堆货架”,也就是垂直扩容。你买更大的仓库、雇更多的员工、加更高的货架——但房租涨了、工资涨了,仓库总有一天会封顶。况且,小仓库只有一位“总调度”(单进程),他每天要记的包裹数量有限,CPU 在高峰期被压到 100% 后,连拆快递盒的动作都开始卡顿。
单机的第二个噩梦是单点故障。仓库里的青册子就是唯一的账本,一旦被老鼠咬烂,整个公司的数据灰飞烟灭。为了保险,你请来一位“记录员”(从节点)每晚抄写账本做备份,可一旦总调度倒地,客户依然只能焦急地等待你手动把记录员扶正——停机时间是按小时计算的。
这就是为什么我们需要 Redis Cluster。它不再把宝压在“一个仓库+垂直扩容”上,而是把地盘切成七块,开七家分拨中心(主节点),每家独立记账、独立处理客户请求。你想扩容?再開一家新分拨中心,把一部分“抽屉”(哈希槽)移交过去即可——这就是水平扩容。你家的总吞吐,从“一个仓库的天花板”,变成了“N 个仓库的叠加”。
用公式来看,单机的容量上限 $C$ 由内存 $M$ 和单核计算力 $P$ 决定:
而你需要的,正是这个 $C_{集群}$——快递总公司在七个区分拨中心同时运转后,吞吐量自然翻了好几番。故事先停在这里,接下来我们去看看分拨中心之间怎么划分地盘。
- 内存上限 $M_{上限}$一台服务器的物理内存是固定的,比如 32GB,存不下再多的缓存数据——这是仓库的物理货架数量。
- 计算力上限 $P_{上限}$单进程 Redis 只能用一个 CPU 核心,每秒最多处理约 10 万条命令——这是总调度一个人处理请求的速度天花板。
- 集群化后的总容量 $C_{集群}$当你有 $N$ 个节点,总容量近似线性增长:
哈希槽原理
七家分拨中心开业了。可问题是:一件收件地址写着“幸福路 128 号”的包裹,究竟该送进 A 分拨中心还是 D 分拨中心?你当然可以靠人工判断——但遇到高峰,判断速度根本跟不上。你需要一套“机械规则”,让任何包裹都能在 1 微秒内自动对号入座。
Redis Cluster 的规则非常简单粗暴又优雅:把整个键空间切成 16384 个槽位。每个分拨中心负责其中一段连续的槽位区间,比如 A 中心负责 0-3000,B 中心负责 3001-6000……至于某个 key 属于哪个槽位,用哈希函数算一下即可:
CRC16 是一种能把任意字符串(快递单号)打散成 0 到 16383 之间数字的哈希算法。同样的 key 永远算到同一个槽。所有分拨中心汇总起来,就是全球唯一的“槽位归属表”——某包裹单号算出槽位 1024,查表发现槽位 1024 在 A 中心,那这个包裹就直接送 A 中心,谁也抢不走。
为什么是 16384 而不是更大的数字?这是设计者的平衡术:槽位表要在每个节点间用 Gossip 协议传播,槽位越多,消息体越大。16384 个槽位的元数据约 2KB,在保证足够细的分片粒度下,不会把网络带宽压垮。也许你还会问:为什么不是 65536?如果槽位太多,一则需要更大的信息包,二则从一个节点迁移一批数据到新节点需要传输更多批次,太重了。
回到快递故事:青册子升级成了“自动分拣机”,包裹传输带经过一个摄像头,扫一下单号,自动落进对应分拨中心的传送口。16384 个抽屉,每个抽屉的门牌号固定,无论包裹从哪个口进来,分拣员都永远不会搞错。
- CRC16 计算对 key 做循环冗余校验,得到一个 16 位的校验值 $h$,这是把任意长度的单号压缩成一个固定位数的数字。
- 取模 16384用 $h$ 对 16384 取余,得到槽位编号 $s$。由于 $h$ 均匀分布,取余后的结果也均匀分布在整个槽空间上:
- 查表定位节点客户端(或者任意节点)维护一张“槽位到节点”的映射表 $T$,$T[s]$ 就是负责槽位 $s$ 的节点地址。于是地址为 true 的包裹单号算完槽位后直达对应节点。
Gossip协议
七家分拨中心分布在城市各区,各自独立运营。但有一个麻烦:A 中心怎么知道 D 中心的仓库还正常? B 中心怎么知道 E 中心快要爆仓了? 如果各个分拨中心之间完全没有沟通,碰到某家中心“着火停摆”,其他中心毫不知情——包裹继续往里送,泥牛入海。
Redis 的答案是:Gossip 协议——让分拨中心之间像八卦的同事一样闲聊。每个节点每秒随机选几个“同事”发送一条问候消息:“嗨,我是 A 中心,我还活着。顺便告诉你:我听到 B 中心好像不行了,D 中心新到了一批货……” 收到消息的人,也会把它听来的其他消息再转述给下一个人。消息像病毒一样在东家长李家短的闲聊中扩散开来,最后全城的人都知道了谁家好、谁家出了状况。
Gossip 协议有几个关键参数:
- PING/PONG:每个节点每秒向随机节点发送 ping,对方收到后回一个 pong,确认活着。
- Gossip 消息体:会携带一部分(约 1/10 的节点)状态信息,这样信息在多次传播后在全集群收敛——像“我听说他听说她听说……”的传话游戏,几轮后所有人对“谁家正常”达成一致。
这种“闲聊式”通信有个巨大好处:没有中心协调员。任何一家分拨中心倒下,不影响其他人继续互相传递消息。全集群的状态不是靠某个“总指挥部”下发指令,而是靠无数个小道消息汇聚成一张动态更新的八卦网——这就是无中心架构的魅力和健壮性。
- 心跳频率 $f$节点每秒发送一次 ping,即 $f = 1Hz$。这是“同事每秒和你打个照面确认你还活着”。
- 传播轮数 $k$消息从节点 A 出发,每一轮随机传给一个邻居,传 $k$ 轮后覆盖全集群。由于每轮广播是指数扩散,$k$ 约等于节点数的对数:
- 收敛时间 $\Delta T$由轮数乘以每轮时间得到,网络快时几乎瞬时完成:
故障转移
某天凌晨三点,B 分拨中心的仓库突然着火了。火势暂时不大,但总调度员已经联系不上。此刻,负责 B 中心那批槽位的包裹还在源源不断地从传送带送来——怎么办?
Redis Cluster 的故障转移机制,像极了一场“火灾演习”。首先,A 中心的员工(节点)发现自己给 B 中心发的 ping 迟迟没有 pong 回复,等超过了 node-timeout(默认 15 秒),A 中心会偷偷在自己心中给 B 记上一笔:主观下线(PFAIL)。注意,这只是 A 的“个人判断”——也许 B 只是网络抖动、也许 A 自己看花眼了。所以 A 会在闲聊时把这条猜测告诉其他分拨中心:嘿,我怀疑 B 不行了,你们也看看?
当超过半数拥有主节点的分拨中心都认为 B 主观下线时,大家达成共识:客观下线(FAIL)。注意——不是 B 自己选的,也不是某个领导拍板的,而是“群众的声音”汇聚成了决议。接下来触发最重要的动作:选举与故障转移。
B 中心平时养着一批“实习副手”(从节点)。火灾发生后,副手们会向其他分拨中心拉票:“我服务质量好、数据最新,选我接管 B 的槽位吧!” 所有拥有主节点的分拨中心按照少数服从多数的原则投票,得票最多的副手成为新主节点——这个过程叫 Raft 式选举。一旦竞选成功,新主节点立刻接管 B 负责的全部哈希槽,对外广播新的槽位归属表。
火灾演练讲解完毕。核心思维是:故障转移不是某个节点说了算,而是整个集群的集体决策。这就解释了为什么分布式系统里“少数服从多数”如此重要——因为没有一个中心大脑来做出决策。
- 主观下线标记节点 A 发现与节点 B 的通信超时,标记 PFAIL(主观下线)。这是“我看见 B 状态可疑”的个人证词。
- 客观下线判定当集群中主节点数量 $N_{master}$ 中,有超过一半都报告 PFAIL 时,状态升级为 FAIL(客观下线)。这是“群众证词”汇聚成实锤:
- 从节点晋级B 的从节点 $R_B$ 向拥有投票权的主节点发起竞选(类似竞选班长),获得多数票后成为新主节点:
客户端路由
分拨中心正常运转了,新问题出现在客户 (客户端) 这边。客户小明要寄一个包裹,单号“SY-2024-1130”。他该把包裹送到 A 中心还是 D 中心?他当然可以先打电话问任意一家分拨中心:“这个单号归谁管?”但如果每件包裹都先问一次再投递,那传送带就堵死了——效率太低。
更聪明的做法是:让客户自己带一张“槽位归属小抄”。小明的手机 App(Redis 客户端)启动时先向任意节点要一份哈希槽分布表,然后本地缓存。之后每来一个 key,客户端本地算一下 CRC16 取模,直接查小抄找到正确的节点,直奔目的地——这就是 smart client(智能客户端)。
但分发中心也会搬仓库:比如扩容时槽位从 A 中心被迁移到 D 中心。小明的小抄可能过期了,他拿着旧地址跑向 A,A 检查发现槽位已经归 D 管,会回一句:MOVED 槽位 1024 请去节点 D——小明立刻更新小抄,把包裹转投 D,并“记下教训”下次不再跑错。这就是 MOVED 重定向。
有一种更微妙的场景:槽位正在从 A 迁移往 D 的半路,小明来了。此时 A 和 D 都管着一部分 1024 槽位的 key。A 会检查这个 key 是否已经迁走:如果还没迁,A 直接自己处理;如果已经迁走了,A 就回一句:ASK 槽位 1024 去 D 节点试试。注意,ASK 和 MOVED 看似相同,但含义不同:ASK 是“你这次去问 D 吧,但别忘了下次还来先问我”,因为它暗示迁移还没完成。
对客户端工程师来说,这一节的收获是:在你的客户端里,一定要处理 MOVED/ASK 重定向逻辑——很多新手只写了单机版本,迁移到集群后缓存一塌糊涂。记住,客户端不只是“发请求的工具”,它还承担着智能路由和故障感知的职责。
- 本地计算槽位客户端先算 key 的槽位,不打电话询问——这是小明先查手机地图,心里有数。
- 查本地映射表 $T_{client}$如果本地表里直接有该槽位的节点地址,发请求过去。
- 收到 MOVED 时更新表节点把最新槽位映射回传,客户端更新 $T_{client}$。
集群扩缩容
回到快递总部的故事。一年后,城市东区发展迅猛,包裹量暴增,E 分拨中心的仓库又塞满了。你面临两个需求:一是把 E 中心的货分流一批出来;二是开一家全新的 F 分拨中心。整个过程必须在“不打烊”的前提下进行——客户还在往 E 中心寄包裹,你总不能关门搬家吧?
Redis Cluster 的扩容是一套精致的“抽屉搬运术”。第一步,新分拨中心 F 装修完毕,带着一沓空抽屉编号加入集群,但还没分配任何槽位。接着,你从 E 中心划出 1000 个槽位,让 E 把这些槽对应的数据打包,批量“搬运”到 F。搬运过程不是一蹴而就的——F 每收到一个 key 就回执一个确认,两边同步进度。搬运期间,正在被搬动的 key 会收到客户端的 ASK 重定向,保证数据不丢失也不写错。
如果你只是想给 E 中心减负,还有其他办法:比如在 E 中心内部再挂一个从节点,让从节点分担一部分读流量。但请注意:从节点不分摊写流量——如果写入量大,必须靠迁移槽位来真正卸担子。扩容的本质,永远是“槽位的再分配和数据的再平衡”。
这套“抽屉搬运术”的精妙之处在于:整个扩容过程,客户端几乎无感知。你不需要停服,不用导数据,Redis Cluster 帮你在线上把所有抽屉均匀地分到了新仓库——这也是它能支撑业务快速扩张的底气所在。
- 发起迁移计划管理员执行
cluster setslot 1024 migrating E,标记槽位 1024 从 E 迁往 F。这是告诉你“这箱货准备出库了”。 - 批量搬运 keyE 中心通过
MIGRATE命令把指定 key 的序列化数据发给 F,F 反序列化后写入自己的内存。每搬完一批,F 回复确认。 - 槽位归属切换所有 key 搬完后,E 广播新表:槽位 1024 归 F。从此客户端再查询时,收到 MOVED 重定向并更新本地小抄。
架构总览
快递公司的故事讲到这里,我们把七家分拨中心、16384 个抽屉、员工八卦网络和火灾演习串成一个全景:
- 无中心化(P2P):所有节点地位平等,没有“总部数据库”;任何节点都可以接受客户端的查询和写入请求。正如每家分拨中心都能接待来访客户。
- 数据与路由分离:数据按哈希槽分片,路由信息靠 Gossip 和重定向机制实时同步。客户端不必代理转发,而是直连节点。
- 线性可扩展:集群总吞吐量 ≈ 节点数 × 单节点吞吐量。数据量增长时,加节点、搬槽位、同步状态,即可近乎线性提升集群能力。
从基础设施的视角看,Redis Cluster 是一个“自带高可用、自带水平扩展、自带路由”的分布式系统。它不依赖外部的负载均衡器、配置中心或仲裁服务,所有能力都内聚在节点自身——这是它被大规模分布式应用广泛采用的根本原因。
从快递公司到分布式系统,Redus Cluster 的架构智慧可以浓缩为一句话:把大问题切成小分片,让每个分片自主运转,再用一套轻量级协议把它们粘合成一个整体。至此,你已经从零理解了 Redis Cluster 的全部核心机制。
- 节点数 $N$集群中主节点的个数,每增加一个主节点,整体并发能力也近乎线性增加。
- 单点吞吐 $QPS_{单节点}$单个 Redis 进程每秒能处理的命令数,通常取决于 CPU 核心和内存带宽。
- 总吞吐 $QPS_{集群}$当槽位分布均匀且没有热点时,集群总吞吐约等于:
AI 应用场景
电商大促的缓存层水平扩展
阶段:模型服务上线后的大规模推理结果缓存
解决的问题:大促期间商品推荐模型的推理结果(如“猜你喜欢”列表)需要高频读取。单机 Redis 在峰值流量下 CPU 100%、内存溢出,导致推荐接口大面积超时。传统方案是买更高配服务器,但成本和天花板都不可接受。
具体案例:某头部电商在双十一前,将推荐缓存从单机 Redis 迁移到 12 节点 Redis Cluster,按用户 ID 哈希分片存储推荐结果。峰值 QPS 从 80 万提升至 700 万。当某节点因流量洪峰疑似故障时,Gossip 探测自动触发故障转移,备用节点 5 秒内接管,用户无感知。整个扩缩容基于在线槽迁移,无需停机。
分布式 AI Agent 的对话状态管理
阶段:大模型多轮对话中的会话状态与上下文存储
解决的问题:大规模 AI Agent 应用中,每个用户的对话上下文(session state)需要快速存取。单机 Redis 无法支撑千万级用户并发会话;同时 Agent 系统需要高可用,不允许缓存层单点故障导致整个 Agent 崩溃。
具体案例:某智能客服 SaaS 平台使用 Redis Cluster 存储用户会话状态(key 为 session_id,value 为对话摘要向量和上下文窗口)。通过哈希槽自动将不同用户的会话均匀分布到集群节点,单点故障由从节点秒级接管,平台整体可用性从 99.5% 提升到 99.99%。
Python 演示
import hashlibimport randomimport time class ClusterNode: def __init__(self, node_id, address): self.node_id = node_id self.address = address self.data = {} self.slots = [] self.alive = True self.replica_of = None # 如果是从节点,记录主节点 node_id def get_slot_count(self): return len(self.slots) def add_slot(self, slot): self.slots.append(slot) def read_key(self, key): return self.data.get(key, None) def write_key(self, key, value): self.data[key] = value class SimpleRedisCluster: SLOT_COUNT = 16384 SLOT_TABLE = {} # slot -> node_id def __init__(self, masters=3, replicas_per_master=1): self.nodes = {} self.masters = masters self._build_nodes(masters, replicas_per_master) self._assign_slots_round_robin() self._bind_replicas() def _build_nodes(self, masters, replicas): for i in range(masters): node = ClusterNode(f"M{i}", f"10.0.0.{i+1}:7000") self.nodes[node.node_id] = node for i in range(masters): for r in range(replicas): rep = ClusterNode(f"R{i}_{r}", f"10.0.1.{i+1}:7{r}") rep.replica_of = f"M{i}" self.nodes[rep.node_id] = rep def _assign_slots_round_robin(self): slot = 0 while slot < self.SLOT_COUNT: for node_id in list(self.nodes): if node_id.startswith("M"): self.nodes[node_id].add_slot(slot) SimpleRedisCluster.SLOT_TABLE[slot] = node_id slot += 1 if slot >= self.SLOT_COUNT: break def _bind_replicas(self): for node in self.nodes.values(): if node.replica_of: pass # 简化:replica 已与主节点建立关联 def _hash_slot(self, key): digest = hashlib.sha256(key.encode("utf-8")).hexdigest() return int(digest[:8], 16) % self.SLOT_COUNT def write(self, key, value): slot = self._hash_slot(key) node_id = SimpleRedisCluster.SLOT_TABLE[slot] node = self.nodes[node_id] if not node.alive: raise RuntimeError(f"Node {node_id} is DOWN, cluster unavailable") node.write_key(key, value) return (slot, node_id) def read(self, key): slot = self._hash_slot(key) node_id = SimpleRedisCluster.SLOT_TABLE[slot] node = self.nodes[node_id] return node.read_key(key) def simulate_failover(self, failed_node_id): failed_node = self.nodes[failed_node_id] if failed_node.replica_of: raise ValueError("只能让主节点故障") master_id = failed_node.node_id # 找到第一个从节点,晋升为新主节点 new_master = None for node in self.nodes.values(): if node.replica_of == master_id and node.alive: new_master = node break if new_master is None: raise RuntimeError("没有可用从节点,无法故障转移") # 移交槽位 for slot, owner in list(SimpleRedisCluster.SLOT_TABLE.items()): if owner == master_id: SimpleRedisCluster.SLOT_TABLE[slot] = new_master.node_id new_master.add_slot(slot) # 数据同步:从节点的 data 与主节点一致 new_master.data = dict(failed_node.data) new_master.replica_of = None failed_node.alive = False print(f"[FAILOVER] {new_master.node_id} 接管了 {master_id} 的槽位") def main(): cluster = SimpleRedisCluster(masters=3, replicas_per_master=1) print(f"集群节点数: {len(cluster.nodes)},槽位总数: {SimpleRedisCluster.SLOT_COUNT}") for node in cluster.nodes.values(): if node.node_id.startswith("M"): print(f"主节点 {node.node_id} 负责槽位: {len(node.slots)} 个") keys = ["user:1001", "session:abc", "cart:888", "order:2024"] for k in keys: slot, node = cluster.write(k, "v") owner = SimpleRedisCluster.SLOT_TABLE[slot] print(f"写入 {k} -> 槽位 {slot} -> 节点 {owner}") print("\n模拟 M0 节点故障...") cluster.simulate_failover("M0") for k in keys: val = cluster.read(k) slot = cluster._hash_slot(k) owner = SimpleRedisCluster.SLOT_TABLE[slot] print(f"读取 {k} -> 槽位 {slot} -> 节点 {owner} -> {val}") if __name__ == "__main__": main()
SimpleRedisCluster.SLOT_COUNT = 16384定义集群的槽位总数,对应 Redis Cluster 的真实规格def _assign_slots_round_robin()轮询给每个主节点分配槽位,模拟集群初始化时的槽位分配过程def _hash_slot(self, key)用 SHA256 哈希并取模得到槽位编号(真实 Redis 用 CRC16)SLOT_TABLE[slot] = node_id维护“槽位到节点”的全局映射表,客户端路由靠查这张表def simulate_failover(self, failed_node_id)模拟主节点故障时,从节点晋升为新主节点并接管全部槽位new_master.data = dict(failed_node.data)简化了复制同步,真实场景中从节点通过主从复制持续同步数据if __name__ == "__main__":标准 Python 入口,保证脚本可直接运行
常见误区
误区我之前看别的系统,比如一致性哈希,它们都有虚拟节点,所以我觉得 Redis Cluster 的 16384 个哈希槽也是一样的东西,是用来解决数据倾斜问题的虚拟节点。
正解哈希槽和虚拟节点是两回事。一致性哈希的虚拟节点是为了让节点在哈希环上分布更均匀;而 Redis Cluster 的哈希槽是整个集群的固定分片粒度,每个槽是真实存在的逻辑容器,节点只是槽的“持有者”。槽的数量是固定的(16384),与节点数量无关;虚拟节点数量通常随物理节点调整。数据倾斜问题的解决,是靠把槽均匀分配给各节点。
为何常见:因为两者都是“在 key 和节点之间加了一层映射”,表面结构相似,容易混淆。
误区我的客户端连上集群后,偶尔会收到 MOVED 错误,是不是我配置写错了?还是集群坏了?我要不要每次收到 MOVED 就立刻重新连接?
正解收到 MOVED 是集群中的正常现象,尤其在槽位迁移、节点扩缩容后。它实际上是客户端更新本地路由表的信号——正如快递员送错仓库后,被纠正一次下次就记住一样。正确的做法是:解析 MOVED 响应中的目标节点地址和槽位号,更新本地映射表后重发请求。如果每次都重新连接,反而会带来大量无谓的连接开销。
为何常见:单机 Redis 没有重定向的概念,习惯了“一个连接走天下”的思维后,遇到重定向错误自然会恐慌。
误区Redis Cluster 号称高可用,那主节点挂了从节点应该毫秒级顶上,用户完全无感知吧?我看那些云厂商都说“秒级切换”。
正解从主节点故障到从节点接管,通常需要 10-30 秒。因为故障检测本身需要等待 node-timeout(默认 15 秒)触发主观下线,然后再通过 Gossip 汇聚成客观下线,之后还要进行选举和槽位接管。如果你在业务代码里没有做重试和熔断,这十几秒的高峰期请求就会大规模超时。云厂商的“秒级”通常是指探测到故障到切换完成的时间,不包含主观下线的等待期。
为何常见:人们对“高可用”的理解往往过于理想化,以为像硬件热备一样瞬间切换,忽略了分布式系统检测故障需要时间。
误区集群写入太慢了,我加几个从节点,读写分离,让从节点分担写压力,性能不就上去了吗?
正解Redis Cluster 的从节点是异步复制主节点的数据,它不承担写请求。写入必须由主节点处理。加从节点只能提升读吞吐和容错性(故障转移时有备用力量),不能提升写性能。想让写性能随集群“线性扩展”,唯一的办法是增加主节点并迁移部分槽位过去,让写入请求分散到更多主节点。
为何常见:受 MySQL 主从读写分离的思维惯性影响,默认以为从节点能分担所有类型的压力。
面试题
Q1. 在 Redis Cluster 中,数据分片依赖的核心结构是什么?
- A.一致性哈希环
- B.16384 个哈希槽← 正确答案
- C.跳表索引
- D.位图数组
答案:B。Redis Cluster 用 16384 个哈希槽作为分片的基本单元,key 先哈希到槽,槽再映射到节点。一致性哈希环是其他系统(如 Memcached 客户端)的方案;跳表是 Redis 内部有序集合的实现,与分片无关;位图数组是布隆过滤器的数据结构。
Q2. 一个 key "session:9527" 要确定它属于哪个节点,正确的计算过程是?
- A.对 key 做 MD5 哈希,再取模节点数量
- B.对 key 做 CRC16 哈希,结果对 16384 取模,再查槽位映射表← 正确答案
- C.对 key 求 ASCII 码之和,再取模节点数
- D.直接向所有节点广播查询
答案:B。Redis Cluster 的定槽公式是 slot = CRC16(key) % 16384,然后再查“槽位→节点”映射表定位。取模节点数量的做法是一致性哈希的思路,不是 Cluster 的做法;ASCII 码之和碰撞概率极高;广播查询是最低效的笨办法。
Q3. 当客户端访问一个正在从 A 节点迁移到 B 节点的槽位时,A 节点返回以下哪种响应?
- A.MOVED
- B.ASK← 正确答案
- C.PONG
- D.ERROR
答案:B。当槽位正在迁移过程中,A 节点会返回 ASK,告诉客户端“这次请去 B 节点试试”。如果迁移已经完成、槽位正式归属 B,则返回 MOVED。PONG 是 Gossip 心跳的响应;ERROR 是通用错误。
Q4. 关于 Gossip 协议,以下哪个描述是准确的?
- A.节点间通过中心协调器同步状态
- B.每个节点定期和所有其他节点交换完整状态
- C.节点随机选择部分节点交换元数据,状态最终收敛← 正确答案
- D.使用 Redis 主从复制协议传递槽位信息
答案:C。Gossip 协议的核心是“随机挑选节点传播消息”,消息在数次传播后传遍全集群,达到最终一致。它不依赖中心协调器;也不与所有节点交换完整状态,那样带宽开销太大;主从复制协议用于数据同步,不用于集群元数据传播。
Q5. 一个主节点发生故障,从节点要接管槽位,触发接管前必须满足什么条件?
- A.从节点自行决定接管
- B.超过半数主节点确认该节点客观下线← 正确答案
- C.客户端发起重连请求
- D.管理员手动执行 failover 命令
答案:B。Redis Cluster 的故障转移必须经过“主观下线(PFAIL)→ 客观下线(FAIL)→ 选举”三部曲。客观下线要求超过半数主节点都确认该节点 PFAIL,然后从节点才能发起竞选。从节点不能自行决定;客户端重连和手动 failover 命令都不是自动触发的前提。
Q6. 关于 Redis Cluster 的客户端路由,smart client(智能客户端)的核心优势是什么?
- A.不需要与集群节点建立连接
- B.缓存槽位映射表,直连目标节点,减少转发开销← 正确答案
- C.自动为所有 key 添加前缀做分片
- D.每次请求先向所有节点广播
答案:B。智能客户端在本地缓存“槽位→节点”映射表,算完哈希槽后直接连接目标节点,省去了中间代理或重定向的开销。它仍然需要与节点建立 TCP 连接;不修改 key;不广播请求。
Q7. 当集群扩容加入一个新节点时,以下哪个说法是正确的?
- A.新节点加入后自动从其他节点抢夺一半数据
- B.需要手动迁移部分哈希槽到新节点← 正确答案
- C.新节点自动成为所有主节点的从节点
- D.新节点只承担读请求
答案:B。新增节点后,它初始不拥有任何槽位。管理员需要通过 CLUSTER SETSLOT 和 MIGRATE 命令把部分槽位迁移过去,不是自动发生的。新节点不会自动抢数据,也不会自动变成从节点,更不是只读节点。
Q8. 关于 Redis Cluster 故障转移过程中,以下哪些说法是正确的?
- A.从节点晋升为新的主节点← 正确答案
- B.旧主节点恢复后自动成为新主节点
- C.故障期间该节点的哈希槽写入会暂时不可用← 正确答案
- D.从节点需要获得超过半数主节点的投票← 正确答案
答案:ACD。故障转移的目标是从节点晋升为主节点,因此 A 正确。旧主节点恢复后会以从节点身份重新加入,不会自动抢回主节点身份,B 错误。故障检测需要等待 node-timeout,期间写入不可用,C 正确。从节点竞选需要超过半数主节点投票(Raft 式选举),D 正确。
Q9. 下列哪些措施可以提升 Redis Cluster 的写吞吐量?
- A.增加从节点数量
- B.增加主节点数量并迁移槽位← 正确答案
- C.将不同 key 均匀分布到不同槽位← 正确答案
- D.增加单节点的 CPU 核心数
答案:BC。增加主节点并迁移槽位,可以让写请求分散到更多节点,提升总吞吐量;key 均匀分布能避免热点集中在少数节点。增加从节点不能分担写请求(从节点异步复制主节点数据);增加单节点 CPU 核心数也不能提升单进程 Redis 的能力,因为 Redis 是单线程处理命令的。
Q10. 下面哪些属于 Redis Cluster 解决的问题?
- A.单节点内存容量有限← 正确答案
- B.单节点 CPU 计算能力有限← 正确答案
- C.单节点故障导致服务不可用← 正确答案
- D.单机 Redis 的持久化文件损坏
答案:ABC。Redis Cluster 通过水平扩展解决容量和计算力上限问题,通过主从节点和故障转移机制解决单点故障问题。持久化文件损坏是单机层面的故障,与集群架构无关——集群是解决“资源不够用”,不是解决“物理磁盘坏道”。