高可用架构
三个阶段的演进,每一步解决上一步的痛点:主从复制解决读扩展和数据冗余,但故障要人工切;哨兵解决自动故障转移,但容量仍受单机内存限制;Cluster 解决水平扩容。
主从复制
全量同步(第一次连接):
- 从库发
PSYNC ? -1(不知道主库 replid,请求全量) - 主库返回
+FULLRESYNC <replid> <offset>,并执行BGSAVE生成 RDB - 主库把 RDB 发给从库,从库清空自身数据后加载
- 生成 RDB 期间主库的新写命令存进
repl_backlog_buffer和从库对应的replication buffer,RDB 传完后再发给从库
增量同步(断线重连):Redis 2.8 起支持。repl_backlog_buffer 是一个环形缓冲区,记录主库最近的写命令。从库重连时带上自己的 offset:
- offset 还在 backlog 范围内 → 只补发缺失的那部分命令
- offset 已被覆盖(断线太久、写入太多)→ 退化为全量同步
所以 repl-backlog-size 要按「最长可能断线时间 × 写入速率」配置,配小了会频繁触发全量同步,而全量同步要 fork + 传大文件,对主库冲击很大。
命令传播:之后主库每执行一条写命令就异步发给从库。注意是异步,所以主从存在延迟,且主库宕机会丢未传播的数据。
链式复制:从库也可以有自己的从库(replicaof 指向另一个从库),减轻主库的传输压力。
哨兵(Sentinel)
独立运行的进程(建议至少 3 个,奇数个),职责是监控、通知、自动故障转移、配置中心。
判定主库下线的两步:
- 主观下线(SDOWN) —— 单个哨兵 ping 主库超过
down-after-milliseconds没响应 - 客观下线(ODOWN) —— 该哨兵询问其他哨兵,达到
quorum个哨兵都认为主库主观下线
两步设计是为了避免因单个哨兵自身的网络问题误判导致不必要的切换。
故障转移流程:
- 哨兵之间用 Raft 类似的算法选举出一个 leader(需要获得超过半数哨兵的投票,所以哨兵总数必须 ≥ 3 且为奇数)
- leader 挑选新主库,优先级依次是:
- 排除下线和断线时间过长的从库
replica-priority配置的优先级高者优先(为 0 则永不被选)- 复制偏移量
offset最大者(数据最新) run_id最小者(纯粹为了确定性)
- 对新主库执行
REPLICAOF NO ONE - 让其他从库
REPLICAOF新主库 - 通过发布订阅(
+switch-master频道)通知客户端新主库地址 - 把旧主库标记为从库,恢复后自动降级
:::warning 脑裂与数据丢失 主库因网络分区与哨兵失联(但仍在运行且有客户端在写),哨兵提升了新主库 → 两个主库同时接受写入。旧主库恢复后被降级为从库,会清空自身数据去全量同步新主库,分区期间写入旧主库的数据全部丢失。
缓解配置:
min-replicas-to-write 1—— 主库至少要有 1 个从库连着才接受写入min-replicas-max-lag 10—— 从库延迟超过 10 秒就不算
这样分区中的旧主库会主动拒绝写入,牺牲可用性换一致性(CAP 里选 C)。 :::
Cluster
分片方案:16384 个哈希槽(hash slot),每个 master 负责一部分。key 的槽位 = CRC16(key) mod 16384。
为什么是 16384 而不是 65536(经典细节题):节点间通过 gossip 交换槽位信息,消息里用位图表示自己负责哪些槽。16384 位 = 2KB,65536 位 = 8KB,对每秒都在互发的心跳包来说差距明显;且 Redis Cluster 设计上不建议超过 1000 个节点,16384 个槽已经足够分配。
请求路由:客户端可以连任意节点。如果 key 不在该节点:
- 返回
MOVED <slot> <ip:port>—— 槽已永久归属别的节点,客户端应更新本地槽位映射并重试 - 返回
ASK <slot> <ip:port>—— 槽正在迁移中,本次请求去目标节点(需先发ASKING),但不要更新映射
智能客户端(如 JedisCluster、go-redis)会缓存槽位表,直接把请求发到正确节点,正常情况下没有重定向开销。
故障转移:Cluster 不需要哨兵,节点之间互相监控。master 被半数以上 master 判定为失败(PFAIL → FAIL)后,它的 slave 发起选举,获得多数 master 投票的 slave 升为 master。所以每个 master 至少要有一个 slave,否则该 master 挂了对应的槽就不可用(cluster-require-full-coverage yes 时整个集群拒绝服务)。
限制(高频追问):
- 多 key 操作要求所有 key 在同一个槽 ——
MSET、SINTER、事务、Lua 脚本都受限。解法是用 hash tag:{user1}:name和{user1}:age只对{}内的内容算哈希,必然落在同一槽 - 不支持多数据库(只有 db 0)
- 事务和 Lua 的原子性只在单节点内保证
三者对比
| 方案 | 解决 | 不解决 |
|---|---|---|
| 主从 | 读扩展、数据冗余 | 自动故障转移、写扩展 |
| 哨兵 | + 自动故障转移 | 写扩展、单机内存上限 |
| Cluster | + 水平扩容、写扩展 | 多 key 跨槽操作、强一致 |
容量够、只要高可用 → 哨兵(运维简单,功能完整);单机内存装不下或写入达到单机瓶颈 → Cluster。不要因为「Cluster 听起来更高级」就上 Cluster,它的多 key 限制会实际影响业务代码。