Redis 作为内存数据库,性能极高但单节点存在两个致命缺陷:一是单机故障导致服务中断,二是单节点内存容量和并发连接数存在上限。生产环境必须引入高可用架构。Redis 官方提供了三套递进式的方案:主从复制(Replication)、哨兵模式(Sentinel)和集群模式(Cluster)。本文深入剖析这三种架构的数据同步机制、故障检测与转移原理,以及槽分片与扩容缩容的完整流程。
一、主从复制(Replication)
主从复制是 Redis 高可用的基础,无论是 Sentinel 还是 Cluster 都依赖复制实现数据冗余。主节点(Master)负责写操作,从节点(Slave)实时同步数据并承担读流量。
1.1 复制拓扑结构
Redis 支持多种拓扑,常见有三种:
一主一从:最简单的架构,Master 故障后需要手动切换,常用于数据备份场景。
一主多从:一个 Master 带多个 Slave,读流量分散到多个从节点。但 Master 的复制压力随 Slave 数量线性增长,每个写命令都要广播给所有从节点。
树状级联:Master 只同步给若干 Slave,这些 Slave 再作为中间节点同步给下一层 Slave。这种结构减轻 Master 的复制负担,但增加了复制延迟和数据不一致的风险。Redis 4.0 引入的 replica-announce-ip 和 replica-announce-port 让跨网络环境的级联复制更加可控。
拓扑选型建议:单主写入量不大、读流量高时,一主多从最简洁。Master 写入量极大(如每秒数万命令)且有大量从节点时,树状级联更稳妥。级联深度建议不超过两层(Master -> 一级 Slave -> 二级 Slave),过深的层级会导致底层从节点延迟难以忍受。跨机房复制时,数据中心的从节点最好由同机房的中间 Slave 同步,而非直连远端 Master。
# 从节点配置文件
replicaof 192.168.1.10 6379
replica-read-only yes
replica-serve-stale-data yes
1.2 全量同步与部分重同步
建立复制关系时,从节点向主节点发送 PSYNC 命令。Redis 2.8 之前只有 SYNC 全量同步,2.8 引入 PSYNC 支持部分重同步,大幅降低了故障恢复时的网络开销。
全量同步(Full Resynchronization)
当从节点首次连接主节点,或主节点的复制积压缓冲区不包含从节点需要的偏移量时,触发全量同步。以下场景必然触发全量同步:
- 从节点初次同步(无历史偏移量)。
- 主节点运行 ID 因重启改变(RDB/AOF 重启后 Run ID 不变,但
SHUTDOWN后冷启动会变)。 - 积压缓冲区溢出,从节点请求的偏移量已被覆盖。
- 主节点执行了
DEBUG SEGFAULT或强制加载 RDB 导致复制状态重置。
全量同步的执行流程:
- 从节点发送
PSYNC ? -1(首次)或PSYNC <replid> <offset>(断线重连但条件不满足)。 - 主节点执行
BGSAVE,生成 RDB 快照文件。 - 主节点将 RDB 文件传输给从节点。
- 从节点清空内存数据,加载 RDB。
- 在
BGSAVE期间主节点收到的写命令,写入复制积压缓冲区(Replication Backlog);RDB 发送完毕后,将这些缓冲命令发送给从节点。
全量同步对主节点是重操作。BGSAVE 采用写时复制(COW),但大量写入仍可能导致内存翻倍。生产环境应定期分析 INFO stats 中的 sync_full 和 sync_partial_ok 计数器。若 sync_full 频繁增长而 sync_partial_ok 极少,说明 repl-backlog-size 设置过小,从节点频繁触发全量同步。理想的比率应该是部分重同步占绝大多数,全量同步仅在新增从节点或长时间网络中断时发生。
部分重同步(Partial Resynchronization)
当网络闪断后快速恢复,若从节点的复制偏移量仍在主节点积压缓冲区内,主节点直接发送断线期间的命令,无需生成 RDB。
部分重同步的触发条件极为严格,必须同时满足三个条件:从节点保存的 master_replid 与当前主节点一致;主节点运行 ID(Run ID)未因重启而改变;请求的偏移量落在 repl-backlog 的有效范围内。任一条件不满足即退回全量同步。因此配置充足的 repl-backlog-size 对于减少全量同步至关重要。
从节点 -> 主节点: PSYNC <master_replid> <repl_offset>
主节点: 检查 repl_backlog 是否包含该 offset
主节点 -> 从节点: +CONTINUE
主节点 -> 从节点: 发送缓冲区中的写命令
关键参数配置:
# 复制积压缓冲区大小,默认 1MB
repl-backlog-size 64mb
# 从节点断线后,缓冲区保留时间
repl-backlog-ttl 3600
# 无盘复制:直接将 RDB 通过网络发送给从节点,不写入磁盘
repl-diskless-sync yes
repl-diskless-sync-delay 5
积压缓冲区默认 1MB,对于写流量大的系统极易溢出。建议按每秒写入量乘以平均断线时间来估算,通常设置为 64MB 或更高。repl-diskless-sync yes 开启无盘复制,避免主节点磁盘 I/O 成为瓶颈,特别适合磁盘性能较差的云服务器。
1.3 复制流程的深入细节
复制 ID(Replication ID)
每个主节点有一个 40 字符的 master_replid。当主节点重启或从节点晋升为主节点时,replid 会改变。从节点同时保存两个 replid:当前主节点的 master_replid 和上一个主节点的 master_replid2,用于处理主节点切换后的部分重同步。
传播与心跳
复制建立后,主节点每收到一个写命令就异步传播给从节点。主从之间通过 REPLCONF ACK <offset> 心跳维持连接,默认每秒一次。主节点根据从节点的 ACK 判断复制延迟。
1.4 读写分离与复制延迟
主从复制的核心价值在于读写分离,但这也是一把双刃剑。由于主节点异步传播命令,从节点数据必然滞后。在以下场景复制延迟尤为严重:
- 从节点执行慢查询或复杂计算,阻塞了命令处理线程。
- 网络带宽不足或跨机房复制。
- 主节点写入量极大,从节点 CPU 满载无法及时处理传播命令。
缓解延迟的策略:
- 监控
master_last_io_seconds_ago和master_repl_offset与slave_repl_offset的差值。 - 对延迟敏感的业务强制走主节点读取,例如支付状态查询、库存扣减后的余额查询。
- 使用
INFO replication实时观察各从节点的延迟情况。 - 考虑 Redis 6.0 引入的 I/O 多线程,提升从节点的命令处理吞吐量。
- 慢查询占满单线程时,复制缓冲区会被阻塞。设置
slowlog-log-slower-than 10000并定期分析慢日志,确保从节点的查询不会拖垮复制管道。 - 网络层面使用专线或内网互通,避免公网复制的带宽抖动。跨可用区复制时可部署中间 Slave 作为聚合节点。
复制延迟的量化监控
Redis 自身提供的延迟指标较为有限,建议结合外部监控方案。Redis 5.0 引入的 INFO latencystats 未直接暴露复制延迟,但可以通过自定义脚本定期比较主从的 master_repl_offset:
#!/bin/bash
MASTER_OFFSET=$(redis-cli -h master_host INFO replication | grep master_repl_offset | cut -d: -f2)
SLAVE_OFFSET=$(redis-cli -h slave_host INFO replication | grep slave_repl_offset | cut -d: -f2)
DELAY=$((MASTER_OFFSET - SLAVE_OFFSET))
echo "replication_delay_bytes $DELAY"
对于写吞吐量在 100MB/s 的集群,10MB 的偏移量差值意味着约 100ms 的复制延迟。建议设置告警阈值:偏移量差值超过 1MB 即触发警告,超过 10MB 触发严重告警。
读写分离的业务设计模式
典型的读写分离有三种模式:激进模式(所有读走从节点,延迟可接受)、保守模式(写后短时间内读主节点,避免读取陈旧数据)、混合模式(按数据类型分级,用户信息读从节点,库存余额读主节点)。推荐混合模式,在代码中封装数据访问层,根据业务语义和一致性要求自动选择节点。
Redis 本身不提供强制读已写的语义。若业务要求读写一致性,要么牺牲性能读主节点,要么在应用层做版本号校验或等待确认机制。
复制偏移量详解
master_repl_offset 和 slave_repl_offset 是判断复制健康度的核心指标。主节点每处理一个写命令就递增偏移量,从节点收到并执行后也递增。两者的差值就是复制延迟的字节数。
redis-cli INFO replication | grep -E "master_repl_offset|slave_repl_offset|lag"
# master_repl_offset:28475291
# slave0:ip=192.168.1.11,port=6379,state=online,offset=28475291,lag=0
lag=0 表示从节点最近一次 ACK 对应的偏移量与主节点完全一致。若 lag 持续大于 1,说明从节点处理速度跟不上主节点写入。可以通过 redis-cli --latency-history 检测网络往返延迟,再结合复制偏移量差值判断瓶颈在网络还是 CPU。
1.5 主从复制的局限性
主从复制解决了数据冗余和读扩展,但有两个核心问题无法解决:
- 无法自动故障转移:Master 宕机后需要人工介入将从节点提升为主节点,并通知客户端切换地址。
- 单节点内存上限:所有数据仍然集中在一台主节点,纵向扩容有物理上限。
这两个问题分别由 Sentinel 和 Cluster 解决。
二、Sentinel 哨兵模式
Sentinel 是 Redis 官方的高可用解决方案,核心职责是监控(Monitoring)、通知(Notification)、自动故障转移(Automatic Failover)和配置提供者(Configuration Provider)。
2.1 Sentinel 部署架构
Sentinel 本身是一个特殊的 Redis 进程,不参与数据存储,只运行 Sentinel 专用命令集。最小推荐部署 3 个 Sentinel 节点,且数量应为奇数。
Sentinel 节点应部署在独立的物理机或可用区,避免与监控的 Redis 节点同机。否则机器宕机会同时带走数据节点和 Sentinel,丧失故障发现能力。三个 Sentinel 节点各自维护独立配置,但会通过 Pub/Sub 频道 _sentinel_:hello 交换信息,保持状态一致。
2.2 Sentinel 核心命令
# 查看监控的 Master 状态
redis-cli -p 26379 SENTINEL master mymaster
# 查看所有从节点
redis-cli -p 26379 SENTINEL slaves mymaster
# 查看其他 Sentinel 节点
redis-cli -p 26379 SENTINEL sentinels mymaster
# 强制故障转移(慎用)
redis-cli -p 26379 SENTINEL failover mymaster
# 动态修改配置(永久生效,写入配置文件)
redis-cli -p 26379 SENTINEL set mymaster down-after-milliseconds 3000
Sentinel 启动后会重写配置文件,追加发现的从节点和 Sentinel 节点信息,因此需要确保 Sentinel 进程对配置文件有写权限。
# sentinel.conf
port 26379
daemonize yes
logfile "/var/log/redis/sentinel.log"
sentinel monitor mymaster 192.168.1.10 6379 2
sentinel down-after-milliseconds mymaster 5000
sentinel parallel-syncs mymaster 1
sentinel failover-timeout mymaster 10000
sentinel auth-pass mymaster yourpassword
关键参数解析:
sentinel monitor mymaster <ip> <port> <quorum>:quorum是将主节点标记为客观下线(ODOWN)所需的最小 Sentinel 同意票数,这里是 2。down-after-milliseconds:主观下线(SDOWN)判断时间。实例超过此时间未回复 PING 即被标记为 SDOWN。parallel-syncs:故障转移后,同时对新主节点进行复制同步的从节点数量。避免过多从节点同时全量同步拖垮新主节点。failover-timeout:整个故障转移过程的超时时间。若超时则视为失败,后续可重试。
2.3 主观下线(SDOWN)与客观下线(ODOWN)
Sentinel 每秒向所有被监控的 Redis 实例(主节点、从节点、其他 Sentinel)发送 PING 命令。
主观下线(Subjectively Down, SDOWN)
单个 Sentinel 实例判断某个节点不可达。若节点在 down-after-milliseconds 内未回复有效的 PING(返回 +PONG、-LOADING 或 -MASTERDOWN 以外的响应,或完全不回复),Sentinel 将其标记为 SDOWN。
SDOWN 只是单个 Sentinel 的视角,可能因网络分区导致误判。
客观下线(Objectively Down, ODOWN)
Sentinel 发现主节点进入 SDOWN 后,向其他 Sentinel 询问该节点的状态。若同意主节点已下线的 Sentinel 数量达到 quorum,则标记为 ODOWN。ODOWN 只针对主节点,从节点和 Sentinel 节点不会有 ODOWN 状态。
Sentinel A: PING Master -> timeout -> SDOWN
Sentinel A: 向 Sentinel B、C 询问 Master 状态
Sentinel B: 同意 Master 已下线
Sentinel C: 同意 Master 已下线
Sentinel A: 同意票数 >= 2 (quorum) -> ODOWN
2.4 Leader Sentinel 选举与故障转移
标记为 ODOWN 后,Sentinel 集群需要选举出一个 Leader 来执行实际的故障转移。Leader 选举采用 Raft 算法。Raft 在分布式共识算法中属于更易理解和实现的类别,Sentinel 并未完整实现 Raft 的所有功能(如日志复制),但借用了其投票机制:
- 每个 Sentinel 收到主节点 ODOWN 后,向其他 Sentinel 请求将自己设为 Leader。
- 每个 Sentinel 每轮只投一次票,先到先得。
- 获得超过半数(majority)选票的 Sentinel 成为 Leader。
为什么是 majority 而不是 quorum?因为 majority 保证任意时刻最多只有一个 Leader,避免脑裂导致的双主问题。
Sentinel A 请求成为 Leader -> Sentinel B 投票同意
Sentinel C 也请求成为 Leader -> Sentinel B 已投票给 A,拒绝 C
Sentinel A 获得 2/3 选票 -> 成为 Leader
故障转移步骤:
- Leader Sentinel 从主节点的从节点中选出一个作为新主节点。选择优先级(
replica-priority)最低、复制偏移量最大、Run ID 最小的从节点。 - Leader 向选中的从节点发送
SLAVEOF NO ONE,使其提升为主节点。 - Leader 向其他从节点发送
SLAVEOF <new_master_ip> <new_master_port>,让它们指向新主节点。 - 当旧主节点恢复上线时,Leader 让它成为新主节点的从节点。
- 更新所有 Sentinel 和被通知的客户端,将主节点地址切换为新主节点。
2.5 客户端集成与故障发现
Sentinel 充当配置提供者,客户端不再硬编码主节点地址,而是连接 Sentinel 获取当前主节点信息。
客户端连接 Sentinel 的两种模式
- 哨兵直连模式:客户端维护 Sentinel 节点列表,启动时轮询获取主节点地址。连接断开后重新查询。这是 go-redis、redis-py 等主流客户端的默认实现。
- Sentinel 代理模式:在应用和 Redis 之间部署独立代理(如 HAProxy 或 Twemproxy),代理本身订阅 Sentinel 的
+switch-master频道自动切换后端。此模式对应用透明,但引入了新的单点,需保证代理自身高可用。
# 查询当前主节点地址
redis-cli -p 26379 SENTINEL get-master-addr-by-name mymaster
# 输出示例:
# 1) "192.168.1.11"
# 2) "6379"
现代客户端(Jedis、go-redis、redis-py、StackExchange.Redis 等)都内置 Sentinel 支持:
- 启动时连接 Sentinel 列表,通过
SENTINEL get-master-addr-by-name获取主节点。 - 订阅
+switch-master频道,实时接收主节点切换通知。 - 当当前连接断开时,自动向 Sentinel 重新查询新主节点并重建连接。
# 客户端 Sentinel 配置示例 (redis.conf 风格)
sentinel:
master: mymaster
nodes:
- 192.168.1.20:26379
- 192.168.1.21:26379
- 192.168.1.22:26379
poolSize: 10
minIdleConns: 5
2.6 脑裂(Split-Brain)问题
什么是脑裂:网络分区发生时,原主节点与 Sentinel 集群隔离,但仍在处理客户端写请求。同时 Sentinel 认为主节点已下线,选举出新主节点。网络恢复后,系统存在两个主节点,数据不一致。
数据丢失:当旧主节点被发现后,Sentinel 会将其降级为新主节点的从节点。此时旧主节点在脑裂期间接收的写数据将全部丢失(因为从节点会清空数据并执行全量同步)。
缓解措施:
# 主节点在至少有一个从节点连接,且延迟不超过 10 秒时,才允许写操作
min-replicas-to-write 1
min-replicas-max-lag 10
这两个参数限制了主节点的写入条件。若主节点与从节点断开(如网络分区导致 Sentinel 和从节点都不可见),写操作被拒绝,避免脑裂期间的数据写入。代价是可用性稍有降低。
另一个工业界的常用方案是配合 ZooKeeper 或 etcd 做分布式锁,确保同一时刻只有一个主节点能写入下游存储。
2.7 Sentinel 的局限性
Sentinel 解决了自动故障转移,但仍是单主架构:
- 只有主节点处理写请求,写入能力无法横向扩展。
- 所有数据存储在一台主节点,内存容量受限于单机。
当单节点内存超过 64GB 或写入 QPS 超过数万时,需要引入 Cluster 分片集群。
Sentinel 生产部署 checklist
| 检查项 | 建议值 |
|---|---|
| Sentinel 数量 | 3 或 5(奇数) |
| Sentinel 与 Redis 是否同机 | 不同机,最好不同可用区 |
down-after-milliseconds | 5-10 秒,太短易误判,太长恢复慢 |
parallel-syncs | 1-3,根据主节点性能调整 |
failover-timeout | 2-3 倍于 down-after-milliseconds |
min-replicas-to-write | 1 |
min-replicas-max-lag | 不超过 30 秒 |
Sentinel 在极端网络分区下仍可能误判。一个常用的防护措施是将 Sentinel 部署在多个可用区,或使用额外的外部健康检查(如负载均衡器的健康探测)作为辅助判断。Sentinel 故障转移并非瞬时完成,整个流程通常在 10-30 秒内,期间写操作会中断。应用层需要有连接断开重连和短暂降级的容错设计。
三、Cluster 集群模式
Redis Cluster 是官方提供的分布式方案,通过数据分片将数据分布到多个节点,同时内置故障转移能力,无需额外部署 Sentinel。
3.1 哈希槽(Hash Slot)机制
Redis Cluster 没有采用一致性哈希,而是固定 16384 个哈希槽(编号 0-16383)。每个键通过 CRC16(key) % 16384 映射到对应槽,每个槽由特定节点负责。
# 计算键所属槽位
redis-cli CLUSTER KEYSLOT user:1001
# (integer) 5474
16384 这个数值的选择是经过权衡的:
- 足够大,让数据在节点间分布均匀。
- 足够小,
CLUSTER NODES和心跳包中携带的槽位信息(位图 2KB)不会成为网络负担。 - 65536 会造成心跳包过大,1024 则分布粒度太粗。
每个节点维护完整的槽-节点映射表,客户端也缓存这个映射。当节点拓扑变化时,通过 MOVED 或 ASK 重定向让客户端更新缓存。
3.2 Gossip 协议与集群通信
Cluster 节点间通过 Gossip 协议交换状态信息,包括节点属性、槽分配、故障标识等。每个节点每秒向随机几个节点发送 PING 包,接收者回复 PONG。
消息类型:
- PING/PONG:心跳与状态交换。PING 携带发送者已知的节点信息,PONG 携带接收者的信息。
- MEET:新节点加入集群时,向已有节点发送 MEET,让它们接纳自己。
- FAIL:节点被标记为 FAIL 时,广播给所有节点加速故障传播。
- PUBLISH:集群模式下广播 Pub/Sub 消息。
Gossip 的优点是无需中心节点,可扩展性强;缺点是信息传播有延迟(cluster-node-timeout 内完成故障发现)。
# cluster.conf
port 6379
cluster-enabled yes
cluster-config-file nodes-6379.conf
cluster-node-timeout 15000
cluster-require-full-coverage yes
cluster-slave-validity-factor 10
cluster-migration-barrier 1
cluster-node-timeout:节点超时时间。超过此时间未收到某节点的 PING/PONG,则将其标记为 PFAIL。cluster-require-full-coverage yes:若某个槽没有被任何节点覆盖,整个集群拒绝服务。设为 no 则只影响缺失槽的请求。
3.3 故障检测与转移
Cluster 没有独立的 Sentinel,故障检测由节点自身完成。
PFAIL(Possibly Fail)
节点 A 超过 cluster-node-timeout 未收到节点 B 的 PONG,将 B 标记为 PFAIL。
FAIL
当大多数主节点(majority of masters)通过 Gossip 确认 B 为 PFAIL,B 被标记为 FAIL 并广播。这与 Sentinel 的 ODOWN 机制类似,但判断主体是集群中的主节点而非 Sentinel。
值得注意的是,cluster-slave-validity-factor 定义为从节点与主节点断开时间超过 cluster-node-timeout * 10 秒时,该从节点在故障转移中将不被考虑选举。这一机制有效排除了因网络分区长期与主节点失联、数据严重滞后的从节点,降低脑裂导致的数据丢失风险。而 cluster-migration-barrier 则控制从节点的动态迁移:当某个主节点拥有超过 barrier 数量的从节点时,集群会自动将富余从节点迁移给没有从节点的主节点,确保所有主节点都有冗余保护。例如集群中 Master A 有两个从节点、Master B 没有从节点时,若 barrier=1,集群逻辑会将 A 的一个从节点迁移给 B。
故障转移流程:
- 从节点发现其主节点被标记为 FAIL。
- 从节点发起选举,向其他主节点请求投票。为确保选举公平性,每个从节点会随机等待一段延迟时间(与复制偏移量相关,偏移量越大等待时间越短,数据越新的从节点越先发起选举)。
- 主节点每轮只能投一票,先到先得。这种先到先得的策略配合偏移量优先级,保证了数据最新的从节点最有可能当选。
- 获得多数主节点选票的从节点晋升为主节点。
- 新主节点接管原主节点的槽位,广播 PONG 更新集群状态。
- 原主节点恢复后成为新主节点的从节点。
选举优先级:与 Sentinel 类似,复制偏移量最大的从节点更有优势。它意味着数据最新,晋升后数据丢失最少。
3.4 MOVED 与 ASK 重定向
客户端初始化时会向集群中某个节点发送 CLUSTER SLOTS 命令,获取完整的槽位分配到节点的映射表,将其缓存在本地。此后客户端直接向负责目标槽的节点发送请求,无需中间转发。Redis 7.0 之后推荐使用 CLUSTER SHARDS 获取更丰富的分片信息,包括副本节点地址。
# 查看槽位分配
redis-cli CLUSTER SLOTS
# 1) 1) (integer) 0
# 2) (integer) 5460
# 3) 1) "192.168.1.10"
# 2) (integer) 6379
# 3) "a1b2c3d4..."
# 4) 1) "192.168.1.11"
# 2) (integer) 6379
# 3) "e5f6g7h8..."
每个条目包含槽范围、主节点信息(IP、端口、Node ID)和从节点信息(可选)。
客户端可以连接任意节点发起请求,但节点只处理自己负责的槽。
MOVED 重定向
当请求的键不在当前节点负责的槽范围内,节点返回 MOVED <slot> <ip>:<port>,客户端需要向目标节点重新发起请求,并更新本地槽映射缓存。
redis-cli -c -p 6379 GET user:1001
# -> Redirected to slot [5474] located at 192.168.1.11:6379
# "Alice"
redis-cli -c 开启集群模式,自动处理 MOVED 重定向。生产环境客户端如 go-redis、JedisCluster 同样自动处理。
ASK 重定向
槽迁移过程中,源节点可能已将部分键迁移到目标节点,但尚未完成。此时源节点返回 ASK <slot> <ip>:<port>,客户端先向目标节点发送 ASKING 命令,再执行原请求。ASK 与 MOVED 的区别在于:ASK 不更新客户端的槽映射缓存,因为迁移是临时的;MOVED 则要求更新缓存。
客户端 -> 节点A: GET key
节点A: 该槽正在迁移到节点B,且 key 已被迁移
节点A -> 客户端: ASK 1234 192.168.1.12:6379
客户端 -> 节点B: ASKING
客户端 -> 节点B: GET key
节点B -> 客户端: value
3.5 槽迁移(Resharding)
槽迁移是 Cluster 扩容和缩容的核心操作,由 redis-cli --cluster reshard 或 CLUSTER SETSLOT 命令触发。
迁移流程(以将槽 5461-10922 从节点A 迁移到节点B 为例):
- 对源节点 A 执行
CLUSTER SETSLOT <slot> MIGRATING <node_B_id>,A 对该槽只接受已有键的查询,新键请求返回 ASK。 - 对目标节点 B 执行
CLUSTER SETSLOT <slot> IMPORTING <node_A_id>,B 准备接收该槽的数据。 - 使用
MIGRATE命令将 A 中该槽的所有键逐一迁移到 B。MIGRATE是原子操作,从源节点读取键值、序列化、发送到目标节点、目标节点恢复、源节点删除。整个过程该键被锁定。 - 所有键迁移完成后,对任意节点执行
CLUSTER SETSLOT <slot> NODE <node_B_id>,将槽正式分配给 B。该命令通过 Gossip 传播到整个集群。
# 自动重新分片,交互式
redis-cli --cluster reshard 192.168.1.10:6379
# 非交互式,将 4096 个槽从源节点迁移到目标节点
redis-cli --cluster reshard 192.168.1.10:6379 \
--cluster-from a1b2c3d4... \
--cluster-to e5f6g7h8... \
--cluster-slots 4096 \
--cluster-yes
迁移期间的数据一致性:
- 正在迁移的键通过
MIGRATE原子操作保证一致性。 - 对于已迁移的键,源节点返回 ASK,客户端自动跳转。
- 迁移不影响其他槽的读写操作。
批量迁移键的内部:
MIGRATE 默认一次迁移一个键。Redis 3.0.6 后支持批量迁移 MIGRATE host port key|"" destination-db timeout [COPY] [REPLACE] [AUTH password] [KEYS key1 key2 ...],大幅减少网络往返。
3.6 扩容与缩容实战
n
扩容(添加新主节点)
假设当前集群为 3 主 3 从,需扩容到 4 主 4 从:
# 1. 启动新节点
redis-server /etc/redis/cluster-6382.conf
# 2. 将新节点加入集群(作为新主节点)
redis-cli --cluster add-node 192.168.1.13:6379 192.168.1.10:6379
# 3. 重新分配槽位(从现有主节点各迁移部分槽)
redis-cli --cluster reshard 192.168.1.10:6379
# 4. 为新主节点添加从节点
redis-cli --cluster add-node 192.168.1.14:6379 192.168.1.13:6379 \
--cluster-slave --cluster-master-id <new_master_id>
缩容(移除节点)
# 1. 将目标主节点的所有槽迁移出去
redis-cli --cluster reshard 192.168.1.10:6379 \
--cluster-from <removing_master_id> \
--cluster-to <receiving_master_id> \
--cluster-slots 4096 \
--cluster-yes
# 2. 移除节点(先移除从节点,再移除主节点)
redis-cli --cluster del-node 192.168.1.10:6379 <slave_node_id>
redis-cli --cluster del-node 192.168.1.10:6379 <master_node_id>
缩容前必须确保槽已全部迁出,否则集群将处于不完整状态。若 cluster-require-full-coverage 为 yes,缺失槽会导致整个集群拒绝服务。
3.7 Cluster 生产实践经验
智能客户端配置
生产环境的 Cluster 客户端需要合理配置连接池和重试策略,避免因拓扑变化引发故障雪崩。
# go-redis Cluster 客户端示例配置
cluster:
addrs:
- 192.168.1.10:6379
- 192.168.1.11:6379
- 192.168.1.12:6379
poolSize: 20
minIdleConns: 5
maxRetries: 3
# MOVED/ASK 重定向后的重试最小退避
minRetryBackoff: 8ms
maxRetryBackoff: 512ms
# 是否只读从节点
readOnly: false
# 路由随机读请求到从节点(readOnly=true 时生效)
routeByLatency: true
开启 routeByLatency 后,客户端会定期探测各节点的延迟,将读请求路由到延迟最低的从节点。这在多可用区部署时非常有用,能自动将请求导向同可用区的副本。
运维监控 checklist
cluster_state:fail:集群处于失败状态,排查是否有节点下线或槽未覆盖。cluster_slots_ok/cluster_slots_fail/cluster_slots_pfail:统计各状态槽位数,PFAIL 槽位需要密切关注是否在向 FAIL 演变。used_memory/maxmemory:各节点内存使用均衡度。热点数据导致槽分布不均时,部分节点可能先达到内存上限。instantaneous_ops_per_sec:各节点 OPS 差异。若某个节点显著高于其他节点,可能存在热点 Key 或槽分配不均匀。- 慢查询日志:
SLOWLOG GET 10定期巡检,大 Key 访问是 Cluster 性能的头号杀手。
大 Key 治理
Cluster 模式下大 Key 的危害被放大:
- 迁移期间
MIGRATE原子操作会长时间阻塞该 Key。 - 访问大 Key 时序列化和网络传输消耗大量时间,阻塞单线程。
- 若大 Key 是 Hash 类型且包含大量 field,客户端一次读取全部内容会造成内存和带宽峰值。
治理方案:业务层拆分为多个小 Key(如用 Hash Tag 将大 Hash 拆成多个小 Hash),或使用 HSCAN、SSCAN 分批次读取。
3.8 Cluster 中的 Hash Tag 与跨槽操作
Cluster 要求多键操作(如 MGET、MSET、事务、Lua 脚本)涉及的所有键必须位于同一槽内。为实现这一点,Redis 提供了 Hash Tag 机制:键名中包含 {} 包裹的子串时,只对该子串计算 CRC16。
# 无 Hash Tag,两键可能分散到不同槽
CLUSTER KEYSLOT user:1001:profile # -> 5474
CLUSTER KEYSLOT user:1001:settings # -> 9821
# 使用 Hash Tag,两键必定同一槽
CLUSTER KEYSLOT {user:1001}:profile # -> 仅对 "user:1001" 计算
CLUSTER KEYSLOT {user:1001}:settings # -> 与上面相同
这使得我们可以将同一用户的相关数据放在同一节点,实现原子多键操作。但滥用 Hash Tag 会造成热点问题:若某个 Hash Tag 下的数据量极大,该节点将承受不成比例的负载。一个合理的设计是:按用户 ID 做 Hash Tag 拆分为多个桶,如 {user:1001}:profile、{user:1002}:profile,让数据仍均匀分布。
Lua 脚本在 Cluster 中的限制
Lua 脚本执行期间不处理其他命令(避免竞态),因此 Cluster 要求脚本访问的所有键必须在同一槽。Redis 3.2+ 引入了 EVAL 的 numkeys 参数预声明键,Cluster 据此判断键位置是否合法。
# 正确:两键在同一槽(通过 Hash Tag)
EVAL "return redis.call('mget', KEYS[1], KEYS[2])" 2 {user:1001}:name {user:1001}:age
# 错误:两键不在同一槽
EVAL "return redis.call('mget', KEYS[1], KEYS[2])" 2 user:1001:name user:1002:name
# -> (error) CROSSSLOT Keys in request don't hash to the same slot
3.9 Cluster 的局限与注意事项
- 多键操作受限: pipeline 和事务(MULTI)中的键必须在同一槽内。若需跨槽,使用 Hash Tag,如
{user:1001}.profile和{user:1001}.settings会被映射到同一槽。 - Pub/Sub 广播:集群模式下 Pub/Sub 消息需要广播到所有节点,大规模集群下性能较差。Redis 7.0 引入 Sharded Pub/Sub 解决此问题。
- MIGRATE 阻塞:单键迁移期间会阻塞该键的访问。大 Hash、大 ZSet 的迁移可能造成明显的延迟尖刺。
- 客户端重试风暴:大规模故障转移时,大量 MOVED/ASK 重定向可能导致客户端重试压力。客户端应实现智能重试退避和槽映射缓存。
四、三种架构对比
| 维度 | 主从复制 | Sentinel 哨兵 | Cluster 集群 |
|---|---|---|---|
| 高可用性 | 无自动故障转移,需人工介入 | 自动故障转移,秒级恢复 | 自动故障转移,内置于集群 |
| 数据一致性 | 最终一致,异步复制有延迟 | 最终一致,异步复制有延迟 | 最终一致,异步复制有延迟 |
| 写入扩展 | 不支持,仅主节点写 | 不支持,仅主节点写 | 支持,多主节点分片写入 |
| 读取扩展 | 支持,一主多从 | 支持,一主多从 | 支持,多主多从 |
| 内存容量 | 单机上限 | 单机上限 | 横向扩展,理论无上限 |
| 故障检测 | 无 | Sentinel 独立进程,基于投票 | 节点间 Gossip,主节点投票 |
| 客户端复杂度 | 低 | 中,需支持 Sentinel | 高,需处理 MOVED/ASK |
| 跨槽操作 | 无限制 | 无限制 | 受限,需 Hash Tag |
| 典型场景 | 数据备份、读写分离 | 中小规模高可用缓存 | 大规模分布式缓存 |
| 最小部署 | 2 节点(1主1从) | 2 节点+3 Sentinel 或 3 节点+3 Sentinel | 6 节点(3主3从) |
选择建议:
- 数据量小于 10GB、写入 QPS 低于 1 万、只需高可用:选择 Sentinel。
- 数据量超过单机内存上限(通常 32-64GB)、写入压力大、需要线性扩展:选择 Cluster。
- 仅做数据备份、读多写少、可接受手动故障切换:主从复制即可。
混合部署实践
某些超大规模系统中,Sentinel 和 Cluster 可以同时存在:核心配置数据量小但要求极高可用,使用 Sentinel 托管;海量缓存数据使用 Cluster 横向扩展。例如电商平台的会话数据(百万级 Key,数据量几 GB)放在 Sentinel 集群,商品详情缓存(数十亿 Key,TB 级)放在 Cluster 集群。这种分层架构兼顾了高可用和扩展性,但也增加了运维复杂度,需要两套监控体系和故障处理流程。
云服务 vs 自建
多数云厂商(AWS ElastiCache、阿里云 Redis、腾讯云 CRS)都托管了 Redis 高可用方案。云服务的优势在于自动备份、网络优化、一键扩缩容和专业的故障处理。但自建方案在成本敏感场景、需要自定义内核参数(如修改 maxclients、调整内存淘汰策略的阈值)或需要与其他基础设施深度整合时更具灵活性。中小型团队优先选择云服务;超大规模或有特殊性能调优需求的团队可以考虑自建。
五、总结
Redis 的高可用架构是一个逐步演进的过程:主从复制奠定基础,Sentinel 实现自动故障转移,Cluster 解决扩展性问题。
主从复制的核心是 PSYNC 机制,理解复制积压缓冲区的大小配置和无盘复制的适用场景,是运维 Redis 的第一步。复制延迟是读写分离必须面对的客观存在,监控和降级策略比盲目追求低延迟更重要。
Sentinel 的价值在于以最小的架构变动(仅增加 Sentinel 进程)实现自动故障转移。理解 SDOWN 到 ODOWN 的判定流程、Raft 选举的 Leader 唯一性、以及脑裂的防护手段,是保障 Redis 服务连续性的关键。min-replicas-to-write 虽牺牲了一点可用性,却是避免脑裂数据丢失的有效防线。
Cluster 是最复杂的架构,但也是大规模 Redis 部署的唯一选择。16384 个槽提供了足够细粒度的数据分布,Gossip 协议保证了无中心节点的可扩展性,MOVED 和 ASK 重定向让客户端透明地处理拓扑变化。槽迁移虽然流程繁琐,但它是扩容缩容不可或缺的机制。生产环境中,应避免大键,合理使用 Hash Tag,并为大规模故障转移设计客户端重试策略。
无论选用哪种架构,持续监控复制延迟、节点内存、连接数和集群状态指标,定期演练故障转移流程,才是 Redis 高可用的最终保障。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。