主从复制与Redis Cluster:高可用架构核心差异与选型实战
开篇为什么非要把这两个东西分清楚聊到Redis高可用很多人第一反应就是“主从复制”。再往前一步知道有哨兵。等业务量真的涨上去了单台Redis内存动不动占满、主节点写入成为瓶颈才想起来有个叫Redis Cluster的选项。这时候问题就来了——既然主从加哨兵已经能自动故障转移了Redis Cluster又多做了什么不都是多台机器一起扛吗这种疑问我见过太多次了。很多团队在从“单机Redis”走向“分布式Redis”时第一版架构都是主从复制加哨兵跑得挺顺。直到某天写入量上来、数据集塞满一台机器的内存才意识到主从复制从根上就没解决“数据分散”的问题——从节点只是主节点的完整拷贝不是分担。Redis Cluster不一样它把整个数据集按槽位打散到多个主节点上每个主节点再配从节点保证可用性。一个是“多机备份同一份数据”一个是“多机各管一部分数据”。这个区别看着简单但牵扯到数据分布、故障转移机制、客户端行为、操作命令限制一环扣一环。这篇文章就把两者从头到尾拆开对比把底层机制和实操里踩过的坑一并讲透。适合正在做Redis架构选型、或者打算从主从模式迁到Cluster模式的开发运维同学参考。1. 主从复制的基础架构与瓶颈所在想理解Cluster的意义得先把主从复制这个老伙计的底细摸清楚。它没有很多人想象的那么弱但也确实有自己的天花板。1.1 主从复制到底怎么工作的主从复制的核心逻辑很简单一个主节点负责接受写请求一个或多个从节点通过复制流实时同步主节点的数据。客户端可以读从节点来分流读压力但写只能走主节点。同步过程用到的机制是PSYNC。第一次连接从节点时从节点发送PSYNC请求主节点做一次BGSAVE生成RDB快照发给从节点同时把快照生成期间产生的新写命令放进复制缓冲区。快照传完后主节点再把缓冲区里的增量命令补发给从节点这算是全量同步。之后进入增量同步阶段主节点持续把写命令通过长连接发给从节点。Redis 4.0之后全量同步也不一定纯靠RDB可以结合AOF缓冲减少大实例全量同步的阻塞时间。这个机制本身很成熟配合主节点和从节点之间的心跳检测从节点掉线重连后能自动增量续传。问题是无论部署多少从节点每个从节点都存着一份全量数据。1.2 哨兵模式补上了什么又留下什么缺口主从复制单独部署时主节点宕机后需要人工把某个从节点提升为新的主节点这在高可用场景下是没法接受的。哨兵Sentinel的出现就是解决这个问题它监控主从节点的存活状态当主节点“看上去”挂了之后哨兵之间通过投票选出新的主节点然后通知所有客户端重新连接新主。所以“主从加哨兵”能实现自动故障转移。比如主节点所在机器网络抖动或者进程崩溃30秒内哨兵会发起切换业务短暂中断后就能恢复写入。很多中小项目靠这套架构撑到百万级数据量是没问题的。但哨兵解决的只是“谁来当主节点”的问题解决不了“数据只有一份全集”的问题。主节点内存满了加从节点没用因为从节点存的是同样多的数据。主节点的写入CPU消耗到极限加从节点也没用因为从节点不承接写请求。这就是主从复制的天花板——存储容量和写入能力都被主节点焊死了。1.3 主从模式下业务水平扩展的几种“土路子”既然主从加哨兵不能在线扩容写入能力业务侧只能想别的办法。我见过比较常见的方案是手动分片比如把用户数据按ID取模分成4个组每组一个“主从加哨兵”的小集群。这在实际项目里确实能撑一段时间但随之而来的问题是取模规则定死之后再扩容到8组时已有数据的映射关系全部变了需要做数据迁移。跨分片的同一事务性操作无法实现比如用户买了两个商品订单和库存分在不同的片上扣库存和生成订单之间的一致性只能靠业务做最终一致性补偿。每一套“主从加哨兵”都需要独立监控、独立告警、独立运维时间长了操作成本明显上升。这就是Redis Cluster出现的历史原因把分片逻辑下沉到Redis自身由它负责数据分布、在线迁移、故障转移业务侧不需要再维护一套分片规则。2. Redis Cluster的分布式骨架槽位、节点与总线Redis Cluster并不是把这套分片规则用简单的方式实现它的设计有相当多的细节。把这些细节弄清楚才算真正理解Cluster和主从复制的区别。2.1 为什么是16384个槽而不是其他数字Cluster内部把整个键空间划分为16384个哈希槽每个主节点负责一部分槽。键会被映射到哪个槽取决于它对键名做CRC16运算后再对16384取模。公式为slot CRC16(key) % 16384。网上经常有人问为什么取16384而不是65536。几个原因节点之间通过心跳消息交换槽位信息消息体里用bitmap表示本节点的槽位列表。16384个槽对应的位图是2KB节点数量再多传输代价也可控。如果换成65536位图变成8KB在节点数量大的情况下会明显加重总线带宽负担。主节点数量通常不会超过1000个在这么个量级下16384个槽的粒度已经足够均衡。CRC16本身产生16位的值低14位作为槽位余数不用额外的哈希算法。这个设计风格非常Redis用起来简单但背后每个数字都抠过性能。键到槽的对应关系确定后每个主节点只需要知道自己负责哪些槽以及每个槽应该在哪个节点上。整个集群的槽位分布信息会同步到每个节点上。以Cluster模式启动的Redis节点之间不是通过普通的客户端端口互相交流而是每个节点额外监听一个端口默认是客户端端口加10000专门跑Cluster总线。2.2 数据分布与无中心化选主机制Cluster的每个节点保存的是全量槽位映射表所以不存在一个“中心节点”来转发请求。客户端连接的任何一个节点如果发现请求的键对应的槽不在本节点上就直接返回MOVED错误把正确的节点地址告诉客户端。这也是Cluster要求客户端必须“聪明”的原因客户端需要缓存槽位与节点的映射关系否则每个请求都可能被重定向。主节点宕机后它的从节点会尝试发起故障转移。这一步和哨兵的仲裁逻辑有相似之处但实现方式不同集群节点通过Gossip协议互相传播状态信息当某个主节点被多数主节点判定为客观下线时它的从节点开始竞选。竞选机制要求从节点获得多数派主节点的投票才能提升为新的主节点。这个投票门槛保证了同一时间内不会出现多个从节点同时上位。有一点值得注意Cluster的故障转移粒度是“主节点”级别。如果一个主节点挂了它的从节点顶上原主节点恢复后变成新主节点的从节点。这个过程中的槽位归属不会因为节点更换而改变槽位还是那些槽位只是承载它们的节点实例变了。2.3 为什么要同时用两个端口很多第一次搭Cluster的人会被端口搞晕为什么启动一个节点要看到两个端口在监听客户端端口通常就是配置里的port用于日常读写请求和命令交互。集群总线端口port10000用于节点间通信包括槽位信息同步、心跳检测、故障上报、选主投票。总线协议是二进制的比普通文本协议更加轻量带宽占用也更小。这种双端口设计把数据流和集群控制流物理隔离。即使某个节点总有大量客户端请求涌入也不至于把集群内部的健康检查消息堵塞掉这是“主从加哨兵”模式完全不存在的一个维度。哨兵是独立的一组进程在监控主从节点而Cluster的每个节点既是数据节点又是控制面参与方。3. 两者本质区别的逐层拆解现在可以把主从复制含哨兵和Cluster放上操作台从几个维度逐一切开。3.1 数据维度全量复制与水平分片主从复制模式下主节点和从节点存放的是同一份完整数据。从节点存在的意义是高可用和读扩展绝不可能通过增加从节点来降低单机内存占用。Cluster模式则彻底改变了数据摆放方式。整个数据集按CRC16结果散落在多个主节点上每个节点只持有部分数据。以3主3从的最小生产配置为例数据按照槽位均匀分布在3个主节点上每个节点各持有约5461个槽内存占用直接就降到单机方案的1/3。数据模型带来的连锁反应很快会在应用层显现单Key和多Key的读写行为不同了。在主从模式下一个事务里可以同时操作任意多个不同的Key因为它们肯定在同一台主节点上。Cluster模式下跨槽位的事务默认无法执行原因很简单这些Key可能落在不同的节点上而Redis的事务本质上是单实例上的命令队列。跨槽位操作的正确打开方式之一是使用Hash Tag比如把同一个用户的多个键写成{user:1001}.cart和{user:1001}.profile。Redis计算槽位时如果键名里有成对的花括号{和}它只对花括号中间的部分做CRC16。这样一来同一个用户下的所有Key都会被映射到同一个槽位进而落在同一个主节点上事务、Lua脚本、管道都能跨多个键执行。3.2 容量扩展维度垂直升级与在线横向扩容主从复制模式下单机内存到达瓶颈后除了换更大内存的机器别无他法。而更换大内存机器就要考虑RDB加载时间、AOF重放时间、全量同步时间停机窗口和数据安全都是问题。Cluster把水平扩展变成了一件相对常规的事情。新增一个主节点让它认领一部分槽位然后通过reshard命令把其他节点上的部分槽迁移过去。数据传输是增量的、在线进行的客户端请求不会中断。槽位迁移过程中旧节点和新节点之间还有一层ASK机制兜底客户端能拿到明确的指引这就保证了迁移期间的数据一致性。从运维角度说这种方式不是零成本但相比“手工分片还要改配置重启”已经是质变。3.3 故障转移维度哨兵仲裁与gossip投票主从加哨兵的故障转移是由哨兵集群独立决策的。哨兵之间通过订阅发布频道互相发现、互相交换状态。主节点被判定为主观下线后需要通过多数派确认才能转成客观下线随后在从节点里选一个数据最完整的晋升为新主节点。Cluster模式的故障转移者不是外挂组件而是节点自身。主节点宕机后它的从节点发起选举向集群中其他主节点请求投票。获得多数派票数后从节点才能完成晋升。这套机制意味着集群节点数越多脑裂保护越强因为选主需要过半主节点参与。两种模式在日常使用中的直接差别是主从加哨兵的客户端连的是哨兵或主节点。主节点切换后客户端必须从哨兵那边重新获取新的主节点地址。Cluster模式下客户端连接任意节点都会被引导到正确的节点上只要客户端实现了Cluster协议它自己就能更新槽位和节点映射。3.4 容量规划与读写模型差异主从模式的读能力可以通过增加只读从节点来扩展写能力则始终受限。Cluster模式每个主节点都能承担写请求整体写入吞吐近似等于所有主节点写入能力之和这在写密集场景下是非常关键的价值。访问模型也因此有巨大变化。主从模式下一个复杂查询可以直接对单个从节点执行KEYS、SUNION、SDIFF等需要全量数据的命令顶多慢一点不影响正确性。Cluster模式下这些命令必须在单槽位内操作否则直接报错。业务上“全量扫描”类需求基本没有生存空间“多Key聚合类操作”也必须在业务层用Hash Tag或客户端聚合。我见过不少从主从迁Clster的项目就是因为代码里用了大量的跨Key计算迁移时被迫重写了一大片业务逻辑。这块成本在选型阶段就该提前评估。3.5 一张表看清主从复制与Cluster的差异对比维度主从复制含哨兵Redis Cluster数据分布从节点保存全量数据数据按CRC16哈希槽分散到多个主节点写扩展能力只能靠主节点单点写入多主节点并行写入吞吐接近线性扩展容量扩展垂直换大内存机器在线reshard水平加节点高可用实现哨兵独立仲裁、选主节点自管Gossip传播状态多数派选主客户端要求需感知主从切换后的新主地址需实现集群协议能识别MOVED与ASK重定向跨Key事务与脚本Key都在同一实例天然支持仅限同槽Key需用Hash Tag约束Key散列运维复杂度哨兵独立部署与监控多套容易乱节点自带集群控制但槽位迁移等需要管理工具适用场景单机内存够用、读多写少、中小规模海量数据、高写入、水平扩展需求强这张表的最后一行很关键。不是所有场景都适合上Cluster如果你的数据量一台16GB内存的机器绰绰有余塞上Cluster只会白白增加运维和开发成本。反过来说如果你已经动了“手动分片”的念头建议直接上Cluster省掉日后给自己埋的雷。4. 从主从模式迁到Cluster模式的实操步骤如果你看完上面的对比决定迁移或者新项目直接使用Cluster这部分内容可以直接作为操作手册。4.1 搭建一套最小的三主三从集群假设你准备了6台机器每台都已经装好Redis版本建议至少6.0以上。这里用测试环境举例正式生产环境建议独立机器部署。每个节点需要修改几个关键配置cluster-enabled yes cluster-config-file nodes.conf cluster-node-timeout 5000 appendonly yes需要注意的细节是cluster-config-file这个文件保存了集群节点信息和槽位分配状态不能手动编辑。节点启动后如果文件内容与实际状态不一致集群可能无法正常加入。多个节点不能共享同一个配置文件。全部节点启动后使用redis-cli命令创建集群redis-cli --cluster create \ 192.168.1.101:6379 192.168.1.102:6379 \ 192.168.1.103:6379 192.168.1.104:6379 \ 192.168.1.105:6379 192.168.1.106:6379 \ --cluster-replicas 1命令里的--cluster-replicas 1表示每个主节点配一个从节点。redis-cli会自动把前3个节点设为主节点后3个设为其从节点并把16384个槽平均分配到前3个主节点上。创建完成后可以通过redis-cli -c -p 6379进入集群模式客户端然后执行127.0.0.1:6379 CLUSTER INFO看cluster_state是否为ok再通过CLUSTER NODES查看每个节点的角色和槽位。4.2 槽位迁移与在线扩容操作后面业务增长、需要加节点的时候扩容流程也值得练一遍。先启动两个新Redis实例一个主一个从配置方式与上面相同。把新主节点加入集群redis-cli --cluster add-node 192.168.1.107:6379 192.168.1.101:6379命令中第二个参数是集群中任意一个现有节点的地址用于完成握手。然后再把新从节点加入并指定它跟随新主节点redis-cli --cluster add-node \ 192.168.1.108:6379 192.168.1.101:6379 \ --cluster-slave --cluster-master-id 新主节点ID节点加入成功后新主节点槽位是空的。此时需要reshardredis-cli --cluster reshard 192.168.1.107:6379这个命令是交互式的它会问你从哪些节点迁移多少个槽到目标节点。如果不希望交互可以带上全部参数。实际操作中我一般建议按节点现有槽数等比迁出比如3个老主节点每个迁出约1024个槽让数据分布重新均衡。reshard过程中Redis会先将槽内数据从源节点导出并导入到目标节点期间这个槽位在这两个节点上都处于“迁移中”状态。客户端请求这个槽的键时可能被引导到源节点也可能被引导到目标节点具体取决于键是否已经迁走。源节点处理不了时会返回ASK告诉客户端去目标节点查。由于这套机制的存在在线迁移期间读写暂时不受影响但网络和磁盘IO会有明显升高。生产环境最好避开业务高峰。4.3 客户端改造要点Cluster模式对客户端提出了更高要求。虽然很多语言的高版本Redis客户端已经内置了Cluster支持但使用前必须确认版本足够新并且开启集群模式。以常见客户端为例逻辑上都类似客户端第一次请求任意节点如果收到MOVED错误就需要更新本地缓存的槽位映射表。收到ASK错误时不能全局更新映射关系只能临时将下一次请求发给目标节点因为ASK只是“这次迁移中这个键恰好被移过去了”的临时指引。客户端初始化时最好通过CLUSTER SLOTS命令一次性加载全量槽位映射而不是靠后续请求踩MOVED一个个填充。从主从模式迁Clster时最头疼的是代码里散落着用Redis做分布式锁、消息队列、会话缓存的地方。分布式锁如果使用单节点Redis实现迁移Clster后必须换成Redlock或者带slot约束的方案。消息队列如果依赖了BRPOPLPUSH这个旧命令在Cluster模式下要么保证键在同一个槽要么升级到Stream方案。会话缓存如果按用户ID做了Hash Tag约束迁移会平滑很多。4.4 迁移过程中的数据校验与回滚方案从老架构迁数据到Cluster推荐用双写加校验的方式。先让业务在写老Redis的同时同步写新Cluster然后写一段脚本对比两边数据。对于缓存类业务可以接受少量不一致等过期重刷对于持久化数据一致性校验要做到位。迁移期间最怕出现存量数据复制完毕、增量数据丢失的情况。稳妥做法是先把读流量切一部分到Cluster跑一段时间观察有没有缓存击穿、读写不一致等隐性问题。全部切完后老的主从集群不要急着销毁至少保留两周方便回滚。回滚这件事一定要提前演练别等出问题临时研究。5. 高频踩坑与问题排查实录Cluster真正运行起来之后你大概率会碰到下面这些问题。我把实操中经常遇到的几种整理出来按出现频率排序。5.1 CLUSTERDOWN集群不想干活了这个错误常出现于集群刚建好但槽位覆盖不完整或者超过半数主节点不可达。排查步骤很简单。redis-cli -p 6379 CLUSTER INFO | grep cluster_state如果输出cluster_state:fail再用CLUSTER NODES看每个主节点的状态确认槽位分布是否有空档。另一种常见情况是误操作了FLUSHALL或者FLUSHDB这会让节点上存储数据清空但不影响槽位分配CLUSTERDOWN不一定会出现。真正会导致CLUSTERDOWN的是某个主节点直接宕机而它的从节点又没有在cluster-node-timeout时间内完成切换。所以如果频繁看到CLUSTERDOWN第一件事是检查主节点宕机时间是否超出了它的从节点选举超时。5.2 MOVED和ASK老是分不清这两个错误是Cluster协议的精髓但也是新手最容易搞混的地方。MOVED表示“这个槽已经确定不在我这里永久归某个节点管你以后直接找它”。客户端收到MOVED后必须更新缓存映射表把槽位指到新节点。ASK表示“这个槽正处于迁移中此刻你问的键被迁到目标节点了我这轮给你指路但槽位的最终归属还没定”。“最终槽位归属确定后后续请求就走MOVED了。从客户端实现的角度收到MOVED更新映射收到ASK则只发送一次重试请求不能更新映射表。5.3 热点Key导致的数据倾斜Cluster按Key的CRC16结果分配槽位这本身是均匀的但业务侧的热点Key会打破均衡。比如某个商品的详情被超高并发查询所有请求都打到一个Key上对应槽位的节点CPU被打满其他节点闲着。应对方式常见有两种一是拆Key把一个大Key拆成多个小Key分散读写压力如商品详情拆成多个分片字段二是加本地缓存在应用层直接拦截热点访问减少打到Redis的请求量。若用的是Hash Tag要额外小心。Hash Tag虽能保证同一组键落在同一个槽但也会让大量不同的键集中到一个节点上必须在设计阶段就评估好热点风险。5.4 多Key操作大量报错不少项目从主从迁移Clster后第一波报错就集中在MGET、DEL、EXEC、Lua脚本这类多Key命令上。报错内容通常会明确告诉你槽位冲突。解决办法是保证这些Key通过Hash Tag落到同一个槽。例如把原本存用户的多个键名改为{user:$id}:cart、{user:$id}:profile。键名的可读性会下降一点但换来的是分布式下的事务能力。还有一点很多人忽略业务代码里如果有直接往Redis塞bigkey的习惯比如把一个几MB的列表塞进一个Key里在Cluster模式下bigkey的负面影响会被放大。因为某一个大Key只会存在于一个槽、一个节点上数据倾斜和节点压力都由单节点承担。应该在写入端做好拆分比如限制单个Key不超过一定大小或者用流式数据结构替代大集合。5.5 Gossip消息对带宽的影响Cluster每个节点每秒都要和其他节点交换心跳信息节点增多后这部分带宽也会水涨船高。如果业务机器带宽本身不富裕建议把cluster-node-timeout适当调大一些减少心跳频率。但这个值太大会延长故障发现时间需要权衡。一般生产环境设置在5到15秒之间比较合适。同时也要关注集群总线上传输的槽位变更消息。频繁reshard会让槽位版本号快速递增导致消息体变大。如果刚做完大规模扩容多观察一段时间总线流量等到稳定后再下线老节点。5.6 从节点不能随便写Cluster模式里从节点默认只能处理只读请求而且只读请求默认不会转发到从节点除非客户端开启了READONLY模式。使用集群的只读从节点分流读流量是有意为之的高级玩法普通业务不建议一上来就这么干因为一旦主节点切换客户端可能短暂读到旧数据。如果确实要让部分非实时性读流量走从节点选择那些业务上能容忍少量延迟的数据并且注意写后读一致性问题。写主节点后立刻读从节点碰上新主从同步延迟会读到旧值。关键业务路径上不要碰这个优化。5.7 迁移后Lua脚本的兼容性问题Lua脚本在单机模式下可以用KEYS[1]、KEYS[2]等同时操作多个不同Key脚本内逻辑自由得很。Cluster模式下Lua脚本必须满足两个条件脚本中涉及的所有Key必须在同一个槽访问的Key最好通过KEYS参数传入而不是在脚本里写死键名。Redis Cluster对脚本键的约束是严格校验的。它会在执行前检查脚本声明的KEYS参数以及脚本内部实际访问的键如果发现键分布在不同槽直接拒绝执行。这会让一些老脚本失效改造时要注意把键名统一到同一个Hash Tag下。5.8 槽位迁移期间的性能毛刺reshard迁移数据时源节点需要把槽内所有键序列化后发给目标节点。大Key多的时候传输过程会产生明显延迟甚至阻塞事件循环。为了降低影响建议迁移前把大Key拆小或者至少确认没有超大集合。一次迁移的槽数不要贪多分多次执行每次迁移后观察集群延迟。最好在业务低峰期做扩容和重新平衡。之前我见过一个极端案例一个节点上有几个几十MB的Setreshard时源节点卡了好几秒线上大面积超时。后来我们设置了一个内部约束单个Key的value不能超过5MB类似问题基本绝迹。让业务侧感知底层存储特性是分布式改造绕不开的一关。最后再分享一点实战体会文章写到这里主从复制和Redis Cluster的差异基本都覆盖了。最后聊一点我用下来的整体感受。很多团队做技术选型时喜欢把“主从加哨兵”和“Redis Cluster”当成两条平行路线一味追求“一步到位”上了Clster结果开发成本爆表反之明明数据集已经塞满单机内存却还硬撑着用主从模式。其实两者之间不是高低之分而是适用场景不同。主从加哨兵架构成熟、简单适合中小规模场景运维轻松Cluster设计精巧扩展性强适合长期演进、数据量和写入规模都比较大的系统。我在实际操作中最大的体会是不要高估自己的业务量也不要低估迁移的代价。如果业务预期三年内数据量不会超过单机Redis存储能力、写入QPS不会压垮单个主节点那么主从加哨兵完全够用。反之只要有“扩容必须重启”“手工分片还要改代码”这些苗头就别犹豫Cluster虽然多了一个维度的工作量但给后续省下的运维成本更可观。真正动手迁移的时候先在测试环境把完整流程演练几遍包括扩容、缩容、节点宕机、槽位迁移。这套动作熟练之后生产切换的底气才会足。文章里涉及的命令行参数和排查思路都是通用的直接拿去用即可。最后再提一句Cluster并不能解决Redis单键性能的极限也不能替代业务侧的缓存设计。架构选型对了底层的使用习惯同样要跟上两者配合才能真正把Redis的高可用和高扩展压榨出来。