目录
正在加载目录…
专栏文章
专栏文章
Redis 专栏
1. Redis:核心数据结构与应用场景 2. Redis 数据结构:底层实现与性能取舍 3. Redis 持久化与复制:RDB、AOF 与主从同步 4. Redis 缓存设计:穿透、击穿与雪崩治理 5. Redis 高可用:哨兵、集群与故障转移 6. Redis 多级缓存:一致性、失效与性能优化

Redis 高可用:哨兵、集群与故障转移

发布于 2026-07-07 10:10 · 最后编辑于 2026-07-31 15:52 · 字数 1,873 👁 117 次阅读

深入 Redis 高可用体系:哨兵模式的故障检测与选主流程、Cluster 集群的分片机制、Codis vs Cluster 选型对比,以及分布式锁的正确实现(SET NX + Lua 脚本)与 Redlock 方案。

目录

章节说明
哨兵模式(Sentinel)故障检测、领导者选举、故障转移流程
哨兵集群的建立哨兵间如何互相发现
Redis Cluster数据分片、故障切换、客户端重定向
Codis vs Redis Cluster两种集群方案的对比与选型
数据倾斜与通信开销Cluster 的两大限制因素
分布式锁SET NX + Lua 脚本,正确实现与常见错误
Redlock(多节点分布式锁)单节点锁的局限与 Redlock 方案
Redis 事务MULTI/EXEC、WATCH、与数据库事务的区别

哨兵模式(Sentinel)

redis sentinel

哨兵解决主库宕机后的自动故障转移问题,通常部署奇数个(≥3)。

主观下线 vs 客观下线

主观下线(SDOWN):
  单个哨兵在 down-after-milliseconds 时间内收不到主库回复
  → 标记主库"主观下线"

客观下线(ODOWN):
  超过 quorum 数量的哨兵都认为主库下线
  → 确认主库"客观下线",启动故障转移
# sentinel.conf
sentinel monitor mymaster 192.168.1.100 6379 2
# 2 = quorum,至少 2 个哨兵同意才算客观下线

领导者哨兵选举(Raft)

确认客观下线后,哨兵们通过 Raft 协议选出 Leader 哨兵来执行故障转移:

1. 候选者:任意一个哨兵(通常是发现主观下线的那个)
2. 发出投票请求:SENTINEL is-master-down-by-addr
3. 其他哨兵投票(先到先得,每轮只投一票)
4. 获得 majority(超过半数)投票者成为 Leader
5. Leader 执行故障转移

故障转移:如何选新主库

Leader 哨兵从从库中选出新主库,筛选标准(按优先级):

1. 过滤掉长时间与主库网络断连的从库
   (断连时长 > down-after-milliseconds × 10 + 主从复制延迟)

2. 按以下优先级选择:
   a. slave-priority 值越小优先(默认 100,0 表示永不成为主库)
   b. 与原主库的 replication offset 越大优先(数据最新)
   c. Run ID 字典序最小(决胜局)

故障转移流程

1. Leader 哨兵向新主库发送 SLAVEOF NO ONE(升级为主库)
2. 监控新主库每秒同步进度,直到 master_replication_offset 追上
3. 向其他从库发送 SLAVEOF 新主库IP PORT(改换门庭)
4. 向原主库(如果恢复)发送 SLAVEOF,降级为从库
5. 更新配置文件,通知客户端新主库地址

哨兵集群的建立

哨兵之间如何互相发现?借助主库的 Pub/Sub

1. 每个哨兵定期向主库的频道 __sentinel__:hello 发布自己的 IP/端口
2. 所有哨兵都订阅该频道
3. 哨兵收到消息 → 发现新哨兵 → 与之建立连接

客户端如何找到主库?
  redis-cli -p 26379 SENTINEL get-master-addr-by-name mymaster
  → 客户端通过哨兵获取当前主库地址(故障转移后自动更新)

Redis Cluster

redis cluster

数据分片:16384 个哈希槽

16384 个 slot 均匀分配给各主节点
key 的 slot = CRC16(key) & 16383

例:3 主节点
  Node A:slot 0 - 5460
  Node B:slot 5461 - 10922
  Node C:slot 10923 - 16383

为什么是 16384 个槽? 作者 antirez 的解释:16384 位 = 2KB,心跳包中发送节点的 slot 信息只需 2KB;16K 个槽在 1000 个节点的集群中每个节点分配约 16 个槽,粒度合理。

客户端重定向(MOVED / ASK)

客户端访问 Node A,但 key 属于 Node B:
  Node A 返回:MOVED 1234 Node_B_IP:6379
  客户端重定向到 Node B

迁移中(MIGRATING / IMPORTING 状态):
  Node A 返回:ASK Node_B_IP:6379
  客户端需要先发 ASKING 命令,再执行操作(临时重定向,不更新路由表)

客户端优化:维护一份 slot → 节点 的路由表(Smart Client),直接路由到正确节点,避免每次都被重定向。

Cluster 的故障转移

类似哨兵,但是集群内的从节点自己发起选举(不需要单独的哨兵进程):

1. 从节点发现主节点下线 → 发起 FAILOVER 广播
2. 其他主节点投票(类 Raft)
3. 获得 majority 投票的从节点升为新主节点
4. 广播更新路由表

Cluster 的限制

- 不支持跨节点的 mget/mset(除非所有 key 在同一个 slot)
- 使用 hash tag {} 保证相关 key 落在同一个节点
  如:{user:1001}:name 和 {user:1001}:age 都在 user:1001 的 slot

- 不支持 MULTI/EXEC 跨 slot 的事务
- Lua 脚本内的所有 key 必须在同一个 slot
# 强制相关 key 落同一 slot
SET {order:1001}:status "paid"
SET {order:1001}:amount "99.9"
MGET {order:1001}:status {order:1001}:amount  # ✅ 同一 slot,可以 mget

Codis vs Redis Cluster

对比CodisRedis Cluster
数据分片1024 个 slot(proxy 路由)16384 个 slot(smart client/重定向)
代理层✅ 有 codis-proxy(无状态,可扩展)❌ 无代理,需智能客户端
原生 Redis 兼容性高(client 无需改造)需要支持 cluster 的客户端
运维复杂度高(proxy + zookeeper)低(全内置)
扩容方式手动迁移槽支持 reshard,但仍需手动触发
跨节点 mget✅ proxy 层聚合❌ 需要 hash tag
多语言支持✅ 标准协议,任意语言需要 cluster 客户端库

选型建议

  • 新项目:优先 Redis Cluster(原生支持,社区活跃,维护成本低)
  • 已有大量非 cluster 代码:Codis 迁移成本低
  • 需要跨节点命令(如 mget 混合 key):Codis 更合适

数据倾斜与通信开销

数据倾斜

原因1:某些 slot 的 key 数量远多于其他(如热点业务集中在某个 hash tag)
原因2:bigkey 导致某个节点内存远高于其他节点

解决方案:
  - 避免超大 hash tag(将一个大 hash tag 拆分为多个)
  - 对 bigkey 进行拆分
  - 监控各节点的内存使用,手动迁移不均衡的 slot

Gossip 通信开销

Cluster 节点间通过 Gossip 协议交换节点状态信息:

每秒:每个节点随机 ping 部分节点
消息大小:每个消息包含 1/10 的节点信息

节点数越多,Gossip 开销越大
建议:单集群节点数 ≤ 1000

分布式锁

单 Redis 节点的正确实现

# 加锁:原子操作(SET NX EX 一条命令)
SET lock_key unique_value NX EX 30
# NX:不存在才设置
# EX 30:30 秒自动释放(防止持有锁的进程崩溃后锁永不释放)
# unique_value:唯一标识(UUID),防止误删其他线程的锁

# 解锁:Lua 脚本保证"判断 + 删除"的原子性
EVAL """
if redis.call('get', KEYS[1]) == ARGV[1] then
    return redis.call('del', KEYS[1])
else
    return 0
end
""" 1 lock_key unique_value

为什么解锁要用 Lua 脚本?

错误的非原子写法:
  1. GET lock_key → "uuid-1"  (验证是自己的锁)
  2. ← 线程被挂起,锁过期,另一个线程获取了锁 →
  3. DEL lock_key  ← 误删了别人的锁!

Lua 脚本将 GET + DEL 变为原子操作,中间不会被其他命令插入。

锁超时续期(看门狗)

持有锁的业务未完成,但锁已超时,需要续期:

# Redisson(Java)的看门狗机制
# 自动每 10 秒检测,如果业务未完成则续期 30 秒
lock = redisson.getLock("lock_key")
lock.lock()  # 自动开启看门狗

# Python 手动实现
def watchdog(redis_client, key, value, ttl, stop_event):
    while not stop_event.is_set():
        time.sleep(ttl / 3)
        # 只有值匹配才续期
        script = """
            if redis.call('get', KEYS[1]) == ARGV[1] then
                return redis.call('expire', KEYS[1], ARGV[2])
            else
                return 0
            end
        """
        redis_client.eval(script, 1, key, value, ttl)

Redlock(多节点分布式锁)

单 Redis 节点的局限

场景:主库宕机,从库还未同步锁信息
  1. 客户端 A 在主库加锁成功
  2. 主库宕机,从库升为新主库(但未同步锁)
  3. 客户端 B 在新主库加锁成功
  → 同时有两个客户端持有"同一把锁"

Redlock 算法

基于 N 个独立 Redis 实例(通常 5 个,互相不复制):

1. 记录加锁开始时间 T1

2. 依次向 N 个 Redis 实例请求加锁
   (每个请求设置较短超时时间,避免某个实例慢导致整体阻塞)

3. 统计成功加锁的实例数:
   - 成功数 > N/2 + 1(多数派)
   - 且加锁总耗时 < 锁的有效期
   → 加锁成功,有效期 = 初始有效期 - 加锁耗时

4. 如果条件不满足 → 向所有实例发送解锁请求
# 示例:5 个独立 Redis 实例
instances = [redis1, redis2, redis3, redis4, redis5]
n = len(instances)
quorum = n // 2 + 1  # = 3

lock_value = str(uuid.uuid4())
lock_ttl = 10000  # 10 秒

acquired = 0
start = time.time_ms()
for r in instances:
    if r.set(key, lock_value, nx=True, px=lock_ttl):
        acquired += 1

elapsed = time.time_ms() - start
if acquired >= quorum and elapsed < lock_ttl:
    # 加锁成功,实际有效期 = lock_ttl - elapsed
    pass
else:
    # 加锁失败,释放已加的锁
    for r in instances:
        release_lock(r, key, lock_value)

Redlock 的争议

Martin Kleppmann 指出:即使使用 Redlock,在进程 GC 暂停(Stop-the-World GC)的极端情况下,仍然可能出现两个客户端同时持有锁。

实践建议

  • 大多数业务场景:单 Redis 主从 + 合理的锁超时 + 幂等设计 就足够了
  • 需要强一致性的场景(如银行转账):使用 etcd/ZooKeeper 的分布式锁(基于 Raft,更强一致性保证)

Redis 事务

MULTI / EXEC

MULTI          # 开始事务(后续命令入队,不立即执行)
SET key1 v1
INCR counter
SET key2 v2
EXEC           # 原子执行所有入队命令(顺序保证,但不回滚)

DISCARD        # 放弃事务,清空命令队列

Redis 事务 ≠ 数据库事务

特性数据库事务(ACID)Redis MULTI/EXEC
原子性✅ 全部成功或全部回滚⚠️ 部分成功(运行时错误不回滚)
一致性⚠️ 弱
隔离性✅ 多级别隔离✅ 事务执行期间不被其他命令打断
持久性依赖 AOF/RDB 配置

Redis 不支持事务回滚:如果 EXEC 执行期间某条命令失败(如对 String 执行 INCR),只有该命令失败,其他命令继续执行。

WATCH(乐观锁)

WATCH key1              # 监控 key1,如果在 EXEC 之前被修改,事务取消

MULTI
INCR key1
SET key2 v2
EXEC                   # 如果 key1 已被其他客户端修改,返回 nil(事务取消)

UNWATCH                # 取消所有 WATCH

WATCH 实现了 CAS(Compare-And-Swap)语义,适合乐观锁场景。

参考资料

← 返回列表

评论 (0)

暂无评论,来留下第一条吧。
登录注册 后才能发表评论