🏰 Redis高可用架构:从单兵作战到军团协作

16 阅读13分钟

考察点: 全量/增量同步、自动故障转移、slot分片、Gossip协议

🎬 开场:一个关于"餐厅"的故事

想象三种餐厅的运营模式:

模式1:单店经营(单机Redis) 🏪

只有1个厨师、1个店面
优点:简单直接
缺点:厨师生病了,餐厅就关门了!

模式2:总店+分店(主从复制) 🏪🏪

1个总店(主)+ 多个分店(从)
总店做菜,分店学习菜谱
优点:分店可以分担接客压力
缺点:总店倒闭了,分店不会做新菜

模式3:总店+分店+店长(哨兵) 🏪🏪👔

有个店长监督:
- 总店倒了,自动提拔一个分店当总店
- 其他分店跟着新总店学习
优点:自动故障转移
缺点:数据量大了,一个总店忙不过来

模式4:连锁餐厅集团(Redis Cluster) 🏪🏪🏪🏪

多个总店,每个负责部分菜系
- A店:川菜
- B店:粤菜
- C店:湘菜
优点:分布式,扩展性强
缺点:管理复杂

Redis的高可用方案就是这样的演进! 🚀


第一部分:主从复制(Replication)👥

1.1 什么是主从复制?

架构:
       [Master]  ← 主节点(读+写)
        /    \
       /      \
   [Slave1] [Slave2]  ← 从节点(只读)
      |        |
   [Slave3] [Slave4]  ← 二级从节点

核心思想:

  • Master接收写操作,Slave复制Master的数据
  • Slave提供读操作,分担Master压力
  • Master宕机,手动切换

1.2 主从复制的实现原理 ⭐⭐⭐⭐⭐

第一步:建立连接

# 从节点配置
127.0.0.1:6380> REPLICAOF 127.0.0.1 6379
# 或在配置文件
replicaof 127.0.0.1 6379

第二步:全量同步(第一次)

时间线:
T1: Slave → Master: "PSYNC ? -1" (请求同步)
T2: Master → Slave: "FULLRESYNC <runid> <offset>"
T3: Master执行BGSAVE,生成RDB快照
T4: Master发送RDB文件给Slave
T5: Slave清空旧数据,加载RDB文件
T6: Master发送复制缓冲区的增量命令
T7: Slave执行增量命令
T8: 全量同步完成!

详细流程图:

Master                           Slave
  |                               |
  |  <──── PSYNC ? -1 ────────   |  (请求同步)
  |                               |
  ├─ 判断是否首次同步             |
  |    ↓ 是                       |
  ├─ 生成 runid 和 offset         |
  |                               |
  |  ──── FULLRESYNC ───────→    |  (全量同步)
  |                               |
  ├─ fork子进程                   |
  ├─ 生成RDB快照                  |
  ├─ 记录复制缓冲区               |
  |    (新写入的命令)             |
  |                               |
  |  ──── 发送RDB文件 ──────→    |
  |                               ├─ 清空旧数据
  |                               ├─ 加载RDB
  |                               |
  |  ──── 发送增量命令 ─────→    |
  |                               ├─ 执行命令
  |                               |
  ✓  同步完成                     ✓

第三步:增量同步(后续)

正常运行期间:
Master执行命令 → 写入AOF → 发送给Slave → Slave执行

示例:
客户端 → Master: SET key1 "value1"
Master → Slave: *3\r\n$3\r\nSET\r\n...
Slave执行: SET key1 "value1"

实时同步!

第四步:部分重同步(断线重连)

场景:Slave断线后重新连接

Slave → Master: "PSYNC <runid> <offset>"
Master判断:
1. offset在复制积压缓冲区内?
   ├─ 是 → 发送缓冲区的增量数据(部分重同步)
   └─ 否 → 全量同步(重新传RDB)

复制积压缓冲区:
┌────────────────────────────────┐
│  默认1MB的环形缓冲区             │
│  [cmd1][cmd2][cmd3]...[cmdN]   │
│           ↑                     │
│         offset                  │
└────────────────────────────────┘

1.3 配置示例

Master配置(redis-master.conf)

# 端口
port 6379

# 绑定IP
bind 0.0.0.0

# 设置密码
requirepass master_password

# 持久化
appendonly yes

Slave配置(redis-slave.conf)

# 端口
port 6380

# 主节点地址
replicaof 127.0.0.1 6379

# 主节点密码
masterauth master_password

# 从节点只读
replica-read-only yes

# 复制积压缓冲区大小(建议设大一点)
repl-backlog-size 10mb

1.4 主从复制的优缺点

✅ 优点:

  1. 读写分离:Master写,Slave读,提高并发
  2. 数据备份:Slave是Master的备份
  3. 高可用基础:为哨兵和集群提供基础

❌ 缺点:

  1. 不能自动故障转移:Master宕机需要手动切换
  2. 复制延迟:网络延迟导致主从不一致
  3. 全量同步开销大:RDB生成和传输耗时
  4. 单点写入瓶颈:所有写操作都在Master

1.5 监控主从状态

# Master查看从节点
127.0.0.1:6379> INFO replication
# Replication
role:master
connected_slaves:2
slave0:ip=127.0.0.1,port=6380,state=online,offset=1000,lag=1
slave1:ip=127.0.0.1,port=6381,state=online,offset=1000,lag=0

# Slave查看主节点
127.0.0.1:6380> INFO replication
role:slave
master_host:127.0.0.1
master_port:6379
master_link_status:up
master_sync_in_progress:0
slave_repl_offset:1000

第二部分:Redis哨兵(Sentinel)🛡️

2.1 什么是哨兵?

哨兵 = 主从复制 + 自动故障转移

架构:
    [Sentinel1] [Sentinel2] [Sentinel3]  ← 哨兵集群(监控)
        ↓           ↓           ↓
      [Master] ←─── 监控 ───→ [Master]
        /   \
       /     \
   [Slave1] [Slave2]

哨兵的职责:

  1. 监控:检查Master和Slave是否正常运行
  2. 通知:节点故障时通知管理员
  3. 自动故障转移:Master宕机时,自动提升Slave为Master
  4. 配置提供者:客户端从Sentinel获取Master地址

2.2 哨兵的工作原理

1️⃣ 监控(Monitoring)

每个Sentinel每秒:
1. 向Master发送PING
2. 向所有Slave发送PING
3. 向其他Sentinel发送PING

检测结果:
- PONG:正常
- 超时:可能下线

2️⃣ 主观下线(Subjectively Down, SDOWN)

单个Sentinel判断:
Master在30秒内没回复PING → 主观下线

配置:
sentinel down-after-milliseconds mymaster 30000

3️⃣ 客观下线(Objectively Down, ODOWN)

多个Sentinel协商判断:
Sentinel1: Master下线了
Sentinel2: Master下线了
Sentinel3: Master正常

投票:2票下线 vs 1票正常
结果:达到quorum(法定人数)→ 客观下线

配置:
sentinel monitor mymaster 127.0.0.1 6379 2  # quorum=2

4️⃣ 选举Leader Sentinel

当Master客观下线后:
1. 各Sentinel投票选出Leader
2. Leader负责执行故障转移
3. 使用Raft算法

投票规则:
- 先到先得
- 每个Sentinel只能投一票
- 获得超过半数票的成为Leader

5️⃣ 故障转移(Failover)

Leader Sentinel执行:

步骤1:从Slave中选择新的Master
选择规则:
1. 排除下线的Slave
2. 排除5秒内没响应的Slave
3. 排除与旧Master断开超过10秒的Slave
4. 选择优先级最高的(replica-priority)
5. 选择复制偏移量最大的(数据最完整)
6. 选择runid最小的

步骤2:让选中的Slave执行SLAVEOF NO ONE → 变成Master

步骤3:让其他Slave执行SLAVEOF新Master

步骤4:将旧Master标记为Slave(恢复后自动变成从节点)

步骤5:通知客户端新的Master地址

流程图:

Master宕机
    ↓
[Sentinel1] 发现超时 → 主观下线
    ↓
询问其他Sentinel
    ↓
[Sentinel2] [Sentinel3] 也认为下线
    ↓
达到quorum → 客观下线
    ↓
选举Leader Sentinel
    ↓
Leader执行故障转移:
    ├─ 选择Slave1作为新Master
    ├─ Slave1: SLAVEOF NO ONE
    ├─ Slave2: SLAVEOF Slave1
    └─ 通知客户端
    ↓
故障转移完成!

2.3 配置示例

Sentinel配置(sentinel.conf)

# 端口
port 26379

# 监控的Master
# sentinel monitor <master-name> <ip> <port> <quorum>
sentinel monitor mymaster 127.0.0.1 6379 2

# 主观下线时间(30秒)
sentinel down-after-milliseconds mymaster 30000

# 故障转移超时时间(3分钟)
sentinel failover-timeout mymaster 180000

# 同时复制新Master的Slave数量(避免雪崩)
sentinel parallel-syncs mymaster 1

# Master密码
sentinel auth-pass mymaster master_password

启动哨兵

# 方式1
redis-sentinel /path/to/sentinel.conf

# 方式2
redis-server /path/to/sentinel.conf --sentinel

# 至少启动3个哨兵(奇数个,避免脑裂)
redis-sentinel sentinel-26379.conf
redis-sentinel sentinel-26380.conf
redis-sentinel sentinel-26381.conf

2.4 客户端连接哨兵

from redis.sentinel import Sentinel

# 连接哨兵
sentinel = Sentinel([
    ('127.0.0.1', 26379),
    ('127.0.0.1', 26380),
    ('127.0.0.1', 26381)
], socket_timeout=0.1)

# 获取Master(写操作)
master = sentinel.master_for('mymaster', socket_timeout=0.1)
master.set('key1', 'value1')

# 获取Slave(读操作)
slave = sentinel.slave_for('mymaster', socket_timeout=0.1)
value = slave.get('key1')

2.5 哨兵的优缺点

✅ 优点:

  1. 自动故障转移:Master宕机自动切换
  2. 高可用:多个哨兵互相监督
  3. 配置简单:相对集群更简单

❌ 缺点:

  1. 不能水平扩展:Master还是单点写入
  2. 主从切换有短暂不可用:切换期间写入失败
  3. 数据不分片:所有数据在Master

第三部分:Redis Cluster(集群)🏢

3.1 什么是Redis Cluster?

集群 = 分布式 + 自动故障转移

架构(33从):
   [Master1]     [Master2]     [Master3]
   slot 0-5460   slot 5461-10922   slot 10923-16383
      ↓             ↓               ↓
   [Slave1]      [Slave2]        [Slave3]

核心特点:

  1. 数据分片:16384个slot,分散到各个Master
  2. 自动故障转移:Master宕机,Slave自动提升
  3. 水平扩展:增加节点即可扩容
  4. 去中心化:无需Sentinel,节点之间通信

3.2 数据分片机制(Slot槽位)

槽位计算

Redis Cluster有16384个槽位(0-16383)

计算公式:
slot = CRC16(key) % 16384

示例:
key = "user:1000"
CRC16("user:1000") = 31234
slot = 31234 % 16384 = 14850

查找:14850在哪个节点?
- Master1: 0-5460 → 不在
- Master2: 5461-10922 → 不在
- Master3: 10923-16383 → 在这!

结果:访问Master3

槽位分配

3个节点均分:
Master1: slot 0-5460     (5461个)
Master2: slot 5461-10922 (5461个)
Master3: slot 10923-16383(5462个)

6个节点均分:
Master1: slot 0-2730
Master2: slot 2731-5460
Master3: slot 5461-8191
Master4: slot 8192-10922
Master5: slot 10923-13652
Master6: slot 13653-16383

Hash Tag(批量操作)

问题:
MGET user:1000 user:1001 user:1002
可能分布在不同节点,无法批量操作!

解决:Hash Tag
key = "user:{1000}"CRC16("{1000}")
key = "user:{1001}"CRC16("{1001}")

规则:
只对{}中的内容计算hash
user:{group1}:1000hash("group1")
user:{group1}:1001hash("group1")
结果:这些key在同一个slot,可以批量操作!

3.3 集群通信(Gossip协议)

节点之间每秒交换信息:

Node1 → Node2: "我负责slot 0-5460,健康状态OK"
Node2 → Node3: "Node1负责0-5460,我负责5461-10922"
Node3 → Node1: "Node2健康,Master3宕机了!"

Gossip消息类型:
- MEET:邀请新节点加入
- PING:检测节点是否在线
- PONG:回复PING
- FAIL:标记节点下线

3.4 故障转移

Master3宕机:
1. 多个节点发现Master3超时
2. 标记Master3为PFAIL(主观下线)
3. 超过半数节点认为下线 → FAIL(客观下线)
4. Slave3自动提升为Master
5. 接管slot 10923-16383
6. 通知其他节点
7. 故障转移完成!

时间:
- 检测:15秒(default node-timeout)
- 选举:几秒
- 总计:20-30秒

3.5 配置和部署

集群配置(redis-cluster.conf)

# 端口(6个节点用不同端口)
port 7000

# 开启集群模式
cluster-enabled yes

# 集群配置文件(自动生成)
cluster-config-file nodes-7000.conf

# 节点超时时间
cluster-node-timeout 15000

# 持久化
appendonly yes

创建集群

# 启动6个节点
redis-server redis-7000.conf
redis-server redis-7001.conf
redis-server redis-7002.conf
redis-server redis-7003.conf
redis-server redis-7004.conf
redis-server redis-7005.conf

# 创建集群(Redis 5.0+)
redis-cli --cluster create \
  127.0.0.1:7000 \
  127.0.0.1:7001 \
  127.0.0.1:7002 \
  127.0.0.1:7003 \
  127.0.0.1:7004 \
  127.0.0.1:7005 \
  --cluster-replicas 1  # 每个Master 1个Slave

# 输出:
>>> Performing hash slots allocation on 6 nodes...
Master[0] -> Slots 0 - 5460
Master[1] -> Slots 5461 - 10922
Master[2] -> Slots 10923 - 16383
Adding replica 127.0.0.1:7003 to 127.0.0.1:7000
Adding replica 127.0.0.1:7004 to 127.0.0.1:7001
Adding replica 127.0.0.1:7005 to 127.0.0.1:7002

集群管理命令

# 查看集群信息
redis-cli -c -p 7000 CLUSTER INFO

# 查看集群节点
redis-cli -c -p 7000 CLUSTER NODES

# 添加节点
redis-cli --cluster add-node 127.0.0.1:7006 127.0.0.1:7000

# 删除节点
redis-cli --cluster del-node 127.0.0.1:7000 <node-id>

# 重新分片
redis-cli --cluster reshard 127.0.0.1:7000

3.6 客户端使用

from rediscluster import RedisCluster

# 连接集群
nodes = [
    {"host": "127.0.0.1", "port": "7000"},
    {"host": "127.0.0.1", "port": "7001"},
    {"host": "127.0.0.1", "port": "7002"},
]

rc = RedisCluster(startup_nodes=nodes, decode_responses=True)

# 使用(自动路由到正确的节点)
rc.set("user:1000", "Tom")
value = rc.get("user:1000")

# 批量操作(使用Hash Tag)
pipe = rc.pipeline()
pipe.set("user:{group1}:1000", "Tom")
pipe.set("user:{group1}:1001", "Jerry")
pipe.execute()

3.7 集群的优缺点

✅ 优点:

  1. 水平扩展:增加节点即可扩容
  2. 高可用:自动故障转移
  3. 高性能:数据分片,并行处理
  4. 无中心节点:去中心化架构

❌ 缺点:

  1. 不支持多数据库:只能用database 0
  2. 批量操作受限:需要使用Hash Tag
  3. 事务受限:跨slot的key不能在同一个事务
  4. 运维复杂:节点管理、数据迁移

第四部分:三种方案对比 🥊

4.1 全面对比表

维度主从复制哨兵集群
架构1主N从1主N从+哨兵M主M从(分片)
故障转移❌ 手动✅ 自动✅ 自动
数据分片❌ 不支持❌ 不支持✅ 支持
写入能力单节点单节点多节点(扩展)
读取能力多节点多节点多节点
最大QPS10万10万100万+
最大内存单机单机集群总和
复杂度⭐⭐⭐⭐⭐⭐
适用场景读多写少中小型应用大型分布式

4.2 场景选择

场景1:小型项目(QPS < 1万)

推荐:单机 或 主从复制

理由:
- 数据量小,单机足够
- 运维简单
- 成本低

架构:
[Master] → [Slave]

场景2:中型项目(QPS 1-10万)

推荐:哨兵模式

理由:
- 自动故障转移
- 高可用保证
- 配置简单

架构:
[Sentinel1] [Sentinel2] [Sentinel3]
    ↓           ↓           ↓
[Master][Slave1] [Slave2]

场景3:大型项目(QPS > 10万,数据量 > 100GB)

推荐:Redis Cluster

理由:
- 水平扩展
- 数据分片
- 高性能

架构:
[M1+S1] [M2+S2] [M3+S3] [M4+S4]...

场景4:金融、交易系统

推荐:哨兵 + 持久化

理由:
- 数据不能丢
- 强一致性要求
- 不能水平扩展(事务要求)

配置:
- appendonly yes
- appendfsync always
- 哨兵监控

4.3 演进路径

创业初期:单机Redis
    ↓
业务增长:主从复制(读写分离)
    ↓
可用性要求:哨兵(自动故障转移)
    ↓
数据量暴涨:Redis Cluster(分布式)

第五部分:实战优化建议 💡

5.1 主从复制优化

# 1. 增大复制积压缓冲区(避免全量同步)
repl-backlog-size 100mb  # 默认1MB太小

# 2. 无盘复制(网络快、磁盘慢时)
repl-diskless-sync yes

# 3. 从节点优先级(故障转移时选择)
replica-priority 100  # 越小优先级越高

# 4. 只读从节点(避免写入)
replica-read-only yes

5.2 哨兵优化

# 1. 合理设置超时时间
sentinel down-after-milliseconds mymaster 5000  # 5秒检测

# 2. 控制并发复制(避免雪崩)
sentinel parallel-syncs mymaster 1  # 一次一个

# 3. 设置足够的quorum
# 3个哨兵:quorum=2
# 5个哨兵:quorum=3

5.3 集群优化

# 1. 合理规划槽位
# 保证每个Master负载均衡

# 2. 使用Hash Tag
# 需要批量操作的key用相同tag

# 3. 监控slot分布
redis-cli --cluster check 127.0.0.1:7000

# 4. 定期重新平衡
redis-cli --cluster rebalance 127.0.0.1:7000

🎓 总结:高可用决策树

      [需要高可用吗?]
            /        \
          否          是
          ↓           ↓
      [单机]    [数据量多大?]
                  /        \
              < 100GB    > 100GB
                 ↓          ↓
           [哨兵模式]   [集群模式]

记忆口诀 🎵

主从复制读写分,
一主多从来分担。
写入主节点执行,
从节点学习来复制。

哨兵监控自动切,
Master宕机自提拔。
投票选举新领导,
高可用性它保障。

集群分片水平扩,
数据分散槽位中。
Gossip协议互通信,
海量数据它能扛!

单机用于小应用,
哨兵适合中等站。
集群支撑大流量,
按需选择最恰当!

面试要点 ⭐

  1. 主从复制原理:全量同步(RDB)+ 增量同步(命令)+ 部分重同步
  2. 哨兵职责:监控、通知、自动故障转移、配置提供
  3. 哨兵选举:Raft算法,超过半数票
  4. 集群分片:16384个slot,CRC16(key) % 16384
  5. 集群通信:Gossip协议,去中心化
  6. 故障转移:选择优先级高、数据完整的Slave
  7. 选择依据:数据量、QPS、可用性要求

最后总结:

Redis高可用就像军队编制 🎖️:

  • 主从复制 = 班(1个班长+几个战士)
  • 哨兵 = 连(有连长监督,自动换班长)
  • 集群 = 军团(多个连协同作战)

记住:没有最好的架构,只有最合适的架构! 🎯

加油,Redis架构师!💪