Redis 高可用架构这事儿,说简单也简单,说复杂能写成一本书。我刚入行那会儿,以为 Redis 挂了就挂了,重启大法好;直到有一天凌晨三点线上订单服务被一个缓存雪崩打趴,才发现单机 Redis 就是颗定时炸弹。后来几年陆续折腾过哨兵、摸过集群,又在各种故障演练里被脑裂、误判、槽位迁移坑过好几轮,才算把这块彻底理顺。
这篇内容我想从“为什么需要高可用”“哨兵和集群各自解决了什么”“到底怎么落地”三个层面展开,把原理、参数、部署、排错一次性讲透。适合正在用 Redis 但还没上高可用方案的同学,也适合已经搭了哨兵或集群但各种报错、failover 玩不溜的运维和后端。我尽量用大白话讲原理,再给可直接抄的配置和命令,保证你看完能上手,也能在面试跟人聊明白。
1. 高可用架构到底在解决什么问题
1.1 单机 Redis 的“命门”
单机 Redis 最大的问题不是慢,而是“一个人扛下所有”。读写都指着这一台实例,内存满了要淘汰、磁盘满了要清理、进程崩了要人肉重启。最尴尬的是,就算你把 Redis 部署得漂漂亮亮,机器停电、网络抖动、物理机宕机这种小概率事件一旦发生,缓存层直接失联,后面的数据库就要瞬间扛住天量流量,分分钟被拖死。
有人会说,那 Redis 支持持久化,重启恢复就行。这话没错,但关键词是“重启恢复”——恢复需要时间,而线上流量可不会等你把 RDB 加载完。AOF 的话恢复更慢,几十 GB 的数据 replay 一遍,业务早就超时一片了。再说了,单机实例连最基本的“故障转移”都做不了,它挂了之后所有读写都失败,根本没有第二条路可走。
所以单机方案只适合三类场景:本地开发、纯缓存且允许丢失、数据量小且不追求 SLA。线上核心链路,单机就是拿命在赌。
1.2 主从复制为什么要加“哨兵”
很多团队的第一步是从单机升级到主从复制:一个 master 负责写,多个 replica 负责读。这样 master 挂了,至少 slave 上还有一份全量数据,可以手动切过去。听起来比单机稳多了,对吧?
但实际用起来会发现一个痛点:主库宕机后,如果你不干预,整个系统依然不可写。因为客户端和主库之间的连接断了,写请求全失败,你总不能每次故障都拉个人起来改配置、改 IP、重启服务。Redis 需要一种机制,能在 Master 下线时自动把某个 Slave 提拔成新 Master,并且让客户端无感地连接到新节点。
这个机制就是哨兵。哨兵的职责可以概括成三件事:监控主从节点是否存活、在主节点故障时自动执行故障转移、把新的主节点信息通知给客户端。它相当于给 Redis 配了一个 7×24 小时的运维值班员,自己判断谁挂了、谁顶上,然后广播地址变更。
1.3 哨兵还是集群:一张表讲清选型逻辑
这两个词经常被混着聊,但定位完全不同。哨兵解决的是“可用性问题”,集群解决的是“容量和性能问题”,两者甚至能叠着用。搞不清这点,架构很容易做拧巴。
| 维度 | 哨兵模式 | 集群模式 |
|---|---|---|
| 核心目标 | 高可用,自动故障转移 | 数据分片 + 高可用 |
| 数据存储 | 所有节点存全量数据 | 按槽位分散存储 |
| 容量上限 | 受单机内存限制 | 理论上可水平扩展 |
| 写扩展性 | 只能写 Master | 所有主节点都能写 |
| 故障转移 | 哨兵自动选举 | 集群内部自动选举 |
| 适用规模 | 几十 GB 以内,读多写少 | 数据量大、写并发高 |
| 复杂度 | 较低 | 较高,客户端需支持集群协议 |
简而言之:如果你主要痛点是“怕 Redis 挂”,选哨兵;如果你 Redis 内存已经几十上百 GB、写并发也上来,单实例撑不住了,那就上集群。现实中也有生产环境用“集群模式 + 每组分片节点挂哨兵”的极端玩法,但日常不建议这么折腾,集群自身已经内置了故障转移能力。
2. 哨兵机制:原理、参数与故障转移全拆解
2.1 三个动作与两类下线的判定
先明确一个认知:哨兵本身也是一个 Redis 实例,只是不做数据存储,专跑监控逻辑。生产环境至少要部署三个哨兵,而不是一个,原因后面会细说。
哨兵的工作可以拆成三块:
- 监控:每隔一定周期(默认 1 秒)向 Master 和 Slave 发送 PING 命令,检查它们有没有响应。
- 通知:当某个被监控的 Redis 实例出问题时,哨兵之间通过发布订阅(Pub/Sub)机制互相通报状态。
- 自动故障转移:当 Master 被判定客观下线后,哨兵集群会协商选出一个 Leader,由它执行 Slave 提升、配置改写和客户端通知。
重点来了,下线判定分两种。
主观下线(Subjectively Down,简称 SDOWN):单个哨兵发现自己 PING 不通 Master,就会把这个实例标记为主观下线。为什么叫“主观”?因为可能只是这个哨兵和 Master 之间的网络闪断了,别的哨兵还能正常连通。
客观下线(Objectively Down,简称 ODOWN):当一个哨兵认为 Master 主观下线后,它会通过sentinel is-master-down-by-addr命令询问其他哨兵。如果收到的确认数量达到 quorum(法定人数),比如 3 个哨兵里 2 个都同意,那这个 Master 才被判定为客观下线,才会触发故障转移。
这里有一个新手常踩的坑:quorum 并不是越大越好。quorum=3 意味着 3 个哨兵都同意才判定下线,如果网络分成了两半,可能两边都凑不出多数票,反而让故障转移迟迟无法触发。生产环境通常是 3 个哨兵配 quorum=2,5 个哨兵配 quorum=3,取“过半即可”的原则。
2.2 哨兵的核心参数详解(附推荐配置)
哨兵的配置方式是在sentinel.conf里写规则,也可以运行时通过命令动态调整。下面这套配置是我在线上验证过的,直接抄作业没问题。
# sentinel.conf port 26379 daemonize yes sentinel monitor mymaster 127.0.0.1 6379 2 sentinel down-after-milliseconds mymaster 5000 sentinel failover-timeout mymaster 15000 sentinel parallel-syncs mymaster 1 sentinel auth-pass mymaster yourpassword sentinel resolve-hostnames no逐条解释一下关键参数:
sentinel monitor <name> <ip> <port> <quorum>:监控哪个主库、多少个哨兵同意才判定客观下线。name 是逻辑名,可以自己起,但要保证所有哨兵一致。down-after-milliseconds:连续多久没响应 PING 就算主观下线。设 5000 意味着 5 秒内无响应就标记。这个值别设太小,否则网络抖动会造成频繁切换;也别太大,否则真宕机时要等半天才恢复。5 秒是我用过比较稳的平衡值。failover-timeout:故障转移的超时时间,包括选主、切换、晋升等步骤的总超时。15 秒一般够用。如果网络慢,可以放大到 20 秒。parallel-syncs:故障转移后,同时向新 Master 发起全量同步的从库数量。设 1 表示一个接一个来,避免多个 Slave 同时复制导致新主库 CPU 和内存飙高。auth-pass:如果 Redis 开了密码,哨兵也需要配置密码才能完成主从身份识别。
还有两个容易忽略的点。第一,所有哨兵的配置文件里,monitor 指向的 IP 必须写 Master 节点地址,而不是某个哨兵自己的地址。第二,哨兵发现 Master 切换后会自动重写自身的配置文件,把 Master 地址更新为新的节点,所以你千万别把 sentinel.conf 设置成只读,否则会报错。
2.3 故障转移的完整流程与脑裂防护
我会用一次真实的故障演练把整个过程串起来。假设有三个节点:
- Master A(10.0.0.1:6379)
- Slave B(10.0.0.2:6379)
- Slave C(10.0.0.3:6379)
- 三个哨兵分别部署在三台机器上
操作步骤:手动 kill 掉 Master A 的 Redis 进程。
发生的事情如下:
- 三个哨兵在 1 秒内都发现 PING A 没响应,各自在本地标记 SDOWN。
- 它们互相用
sentinel is-master-down-by-addr命令确认,3 个里至少 2 个同意,Master A 升级为 ODOWN。 - 哨兵们开始选举 Leader。这里用的是类似 Raft 的机制:每个哨兵都有投票权,谁先发起投票、谁获得多数票谁就是 Leader。只有 Leader 才有权限执行故障转移。
- Leader 在存活 Slave 里挑选新 Master。选举规则优先级是:配置优先级(slave-priority 越小越优先)→ 数据最完整(复制偏移量最大)→ 运行 ID 最小。
- 选定新 Master(假设是 B),Leader 向 B 发送
slaveof no one,让 B 变成独立的 Master。 - Leader 通知其他 Slave(C)执行
slaveof B,让它们重新指向新主库。 - 所有哨兵更新自己的配置,把 Master 指向 B,并通过 Pub/Sub 对外发布 switch-master 事件。客户端如果实现了哨兵协议,会接这个事件自动刷新连接地址。
整个过程看起来挺顺畅,但我必须提醒你一个容易被忽视的风险:脑裂。假设 Master A 和哨兵们之间的网络断了,但 A 本身还活着。哨兵们判定 A 客观下线后把 B 提拔为 Master,此时 A 并不知情,它还在继续接受写请求。A 的连接恢复后,A 会发现自己变成了别人的 Slave,于是主动将自己的数据同步给 B——但同步是单向的,A 在断连期间产生的写入数据,会被 B 的全量数据覆盖掉,这些数据直接丢失。
解决脑裂的办法不是让哨兵更聪明,而是从写入侧做限制。Redis 官方提供了两个配置来保护一致性:
min-replicas-to-write 1 min-replicas-max-lag 10意思是最少有 1 个从库与主库的复制延迟在 10 秒以内,主库才接受写入。如果脑裂导致从库全部失联,主库的写入条件不满足,它就会拒绝写入,从根源上避免断连期间的脏数据产生。这两个参数我在生产环境强制开启,宁可损失短暂可用性,也不接受不可追踪的数据丢失。
注意:如果你对数据一致性极度敏感,连
min-replicas都不能完全信任。Redis 的复制本身是异步的,任何宕机切换方案都可能丢几条最新的写请求。高可用和高一致在 Redis 里天然存在取舍,别指望它替代传统数据库的事务能力。
3. 集群机制:分片、通信与选主到底怎么运转
3.1 为什么是 16384 个哈希槽
聊到 Redis Cluster,几乎所有人都会问:为什么数据分片偏偏用 16384 个槽,而不是 1024、65536?网上有各种说法,我按自己的理解给你讲一遍。
Cluster 不是把 key 直接哈希到节点,而是先把 key 做 CRC16 校验,然后取模 16384 得到一个槽位编号。每个主节点负责一段连续的槽位范围。比如三主集群,节点 A 管 0–5460,节点 B 管 5461–10922,节点 C 管 10923–16383。
为什么是 16384?官方给出的解释有几个:
- 16384 个槽位在节点数较少时,每个节点负责的槽位足够大,数据能均匀分布。
- 槽位信息需要定期在节点之间通过 Gossip 协议传播,槽位数量太多会导致心跳包变大,浪费带宽。比起 65536,16384 的心跳包载荷更小。
- 16384 在特定网络包大小下有更好的压缩表现。
但其实更直观的理解是:16384 是一个在“数据分散粒度”和“元数据传播开销”之间折中的结果。节点才几十个,槽位搞几万个没意义;槽位太少,又容易出现数据倾斜。
实际工作中,你要关心的不是 16384 这个数字本身,而是一件事:Redis Cluster 要求一个 key 只能属于一个槽,所以跨槽位的多 key 操作(比如 mget 跨节点、事务跨节点)默认不支持。解决手段是使用哈希标签(hash tag):把 key 写成{user1001}.profile和{user1001}.cart的形式,Redis 只会对花括号内的内容做 CRC16,这样两个 key 就能落到同一个槽位,从而支持事务和批量操作。这是集群下做业务设计最核心的技巧之一。
3.2 Gossip 协议与集群总线
集群节点之间互通的链路叫集群总线(Cluster Bus),用的是另一个端口,默认是节点端口 + 10000。比如 Redis 监听 6379,总线就是 16379。很多人排查集群网络问题时容易忽略这个端口,结果 Redis 的数据端口通了、总线端口被防火墙挡住,节点之间始终没法完成握手和心跳,折腾半天才发现是少开放了一个端口。
集群里的节点是怎么互相感知的?靠的是 Gossip 协议。每个节点每 100ms 会向部分随机节点发送 PING,附带自己知道的其他节点状态;收到消息的节点会更新自己的集群元数据,并继续向其他节点扩散。这种“病毒式传播”的好处是:节点数量多时依然能较快收敛,且没有中心节点,任何一个节点挂了都不影响元数据传播。
但 Gossip 也带来了一个隐患:节点状态的变化不是瞬间全局同步的,会有毫秒级的传播延迟。所以 Cluster 的故障判定也设计了主观/客观两套逻辑:
- 节点 A 向节点 B 发 PING 后,超过
cluster-node-timeout(默认 15 秒)没收到 PONG,A 就认为 B 主观下线,并在集群里广播。 - 当集群中超过一半的主节点都认为 B 主观下线,B 就被标记为客观下线。此时如果 B 是主节点,集群会触发主从切换。
这里有个重要的细节:只有持有 B 的从节点的节点,才有资格发起故障转移投票;而整个集群中,每个主节点只有一票。选举逻辑简而言之就是“少数服从多数 + 发起方从拥有从节点的节点开始拉票”,和哨兵的 Leader 选举类似,但作用范围是整个集群。
3.3 集群故障转移与槽位迁移
Cluster 的故障转移和哨兵不同的地方在于:不需要外部哨兵进程,主从切换完全由集群内部完成。假设某个主节点挂了,它下面的从节点会先等一段时间(计算公式和cluster-node-timeout有关),然后向集群里其他主节点发起投票申请。拿到超过半数的票后,从节点就会把自己晋升为主节点,并接管原主节点负责的槽位。
从节点晋升后,它广播自己“我变成主节点了”。其他节点收到消息后,会把路由表里对应槽位的 owner 更新掉。原来的挂掉主节点即便后来恢复了,重新加入集群时也会发现自己已经有主了,于是自动降级成从节点。这套机制在多数场景下是自动的,不需要人工干预。
真正需要人工参与的是“槽位迁移”。比如你要给集群扩容,加一个主节点进来,不迁移槽位的话,新节点就是个光杆司令,没有任何数据。集群的扩容公式和步骤大致如下:
- 新节点通过
cluster meet命令加入集群。 - 用
redis-cli --cluster reshard <ip>:<port>发起重新分片。 - 输入要迁移的槽位数量,比如 4096,再输入接收槽位的节点 ID。
- 选择源节点:可以是所有节点分担,也可以指定某个节点全部迁出。
- 集群会逐个槽位搬运数据,搬运过程中 key 还留在旧节点,客户端访问该槽位时,旧节点看到 key 正在迁移会返回
ASK重定向,让客户端去新节点查找。
整个过程可以热执行,不用停机,但会占带宽和 CPU。我的经验是,大集群迁移槽位前先量一下 key 数量,如果某个槽位的 key 特别多,迁移时间会非常久,最好拆到低峰期做。迁移途中不要手动改集群配置,容易把元数据搞乱。
4. 从零搭建:哨兵与集群的实操记录
4.1 三节点哨兵模式部署实战
哨兵部署我觉得最顺手的方案是三台机器,每台机器上既放一个 Redis 实例又放一个哨兵,这样既省机器,又能保证哨兵和 Redis 进程的相互独立(进程是独立的,崩溃不会互相影响)。
假设三台机器 IP 分别是 10.0.0.1、10.0.0.2、10.0.0.3。
第一步,先在 10.0.0.1 上装好 Redis,配置 redis.conf 关键项:
bind 0.0.0.0 port 6379 daemonize yes dir /var/lib/redis requirepass redispass masterauth redispass appendonly yesmasterauth特别重要。主从复制和故障转移后,Slave 连新 Master 认证需要这个密码。很多人只配 requirepass,结果从库一直同步报错:NOAUTH Authentication required。
第二步,在 10.0.0.2 和 10.0.0.3 上同样安装 Redis,并在 redis.conf 里加一行:
replicaof 10.0.0.1 6379然后各自启动,确认info replication里master_link_status:up。
第三步,三台机器上分别写一份 sentinel.conf:
port 26379 daemonize yes sentinel monitor mymaster 10.0.0.1 6379 2 sentinel down-after-milliseconds mymaster 5000 sentinel failover-timeout mymaster 15000 sentinel parallel-syncs mymaster 1 sentinel auth-pass mymaster redispass启动方式:
redis-sentinel /etc/redis/sentinel.conf启动后可以用一条命令看哨兵是否已经发现所有节点:
redis-cli -p 26379 sentinel master mymaster redis-cli -p 26379 sentinel replicas mymaster redis-cli -p 26379 sentinel sentinels mymaster如果输出里出现了三个哨兵的 IP,说明集群关系已经建立。然后你可以直接kill -9杀掉 10.0.0.1 的 Redis 主进程,等 10 秒左右,再查哨兵的 master 是谁,你会发现主节点已经切到另一台机器了。切换完成后,老主库的配置文件里被写入replicaof <新主库IP>,它恢复了也会自动变成从库。整个过程完全无人工介入,这就是哨兵的价值。
注意:哨兵模式下,客户端连接不能只用原本地地址。你要使用支持哨兵的客户端,比如 Java 的 Lettuce 的
RedisSentinel模式,配置哨兵节点地址和 master 名称。否则主库 IP 变化后,客户端依然连旧地址,照样故障。
4.2 三主三从集群搭建实战
集群的搭建比哨兵直观,但步骤略多。最稳的方案是六台机器(或容器):三个主节点负责写,三个从节点负责冗余。
每台机器的 redis.conf 都要开启集群模式:
port 6379 daemonize yes dir /var/lib/redis appendonly yes cluster-enabled yes cluster-config-file nodes-6379.conf cluster-node-timeout 15000 requirepass redispass masterauth redispass注意cluster-enabled yes这行是灵魂配置。没有它,Redis 不会启动集群协议。cluster-config-file会自动生成,不用手动建,它会记录当前节点的集群状态。
六个节点全部启动后,开始建集群。旧版是redis-trib.rb,新版直接内置在 redis-cli 里,用起来更简单:
redis-cli -a redispass --cluster create \ 10.0.0.1:6379 10.0.0.2:6379 10.0.0.3:6379 \ 10.0.0.4:6379 10.0.0.5:6379 10.0.0.6:6379 \ --cluster-replicas 1--cluster-replicas 1表示每个主节点配一个从节点,所以前三个是主节点,后三个自动成为它们的副本。执行过程会先计算槽位分配方案,然后问你是否同意,输入 yes 就开始跑。
跑完可以验证:
redis-cli -a redispass -c -h 10.0.0.1 -p 6379 cluster info看到cluster_state:ok就是健康状态。-c是 cluster 模式,客户端会自动处理 MOVED 重定向,这个参数很重要。再用cluster nodes查看节点角色和槽位分布。
设置一个测试 key:
redis-cli -a redispass -c -p 6379 set foo bar如果该 key 的槽位不在 10.0.0.1 上,cluster 模式会自动跳转写入,并返回你实际写入的节点。这就是集群“自动路由”的验证。
4.3 日常监控与读写分离配置
高可用不是搭完就完事,日常监控必须跟上。我常用的监控工具是 Prometheus + redis_exporter + Grafana。redis_exporter 能采集同步延迟、内存、命中率、集群槽位迁移状态、哨兵主节点变化等指标。关键是这几个监控项别漏:
- 主从复制延迟:
master_repl_offset和slave_repl_offset的差值,超过阈值要告警。 - 集群节点数变化:
cluster_known_nodes减少说明有节点掉线。 - 拒绝连接数:
rejected_connections飙高说明 maxclients 配置不足。 - 哨兵切换事件:监听
+switch-master日志,切换次数多了说明网络环境不稳定。
读写分离配置方面,哨兵模式和集群模式做法不同。
- 哨兵模式下,客户端可以用 Lettuce 的 RedisSentinel 配置一组读写分离策略:读请求分发到从节点,写请求只走 Master。但要注意,从节点有最多几秒的复制延迟,强一致性读业务别走从节点。
- 集群模式下,官方客户端(Lettuce、Jedis Cluster、redis-py-cluster)默认是读写都走主节点的,没做读写分离。你要复用从节点的话,可以用客户端的
readFrom策略(Lettuce 支持REPLICA),或者在业务侧自己按 key 做路由。不建议在集群里强行做读写分离,因为延迟收益不大,复杂度倒是会上去不少。
5. 高可用落地中的常见问题与排查实录
5.1 误判下线、脑裂与数据丢失
我在真实环境里遇到过两类让运维最头疼的问题:误判和脑裂。
误判的场景通常是这样的:某个大 key 在执行慢操作,比如KEYS *扫描或超大集合的SUNIONSTORE,Redis 主线程被卡住好几秒,期间哨兵的 PING 一直没得到响应,于是被判主观下线。接着多个哨兵确认后触发客观下线,主从切换。实际上老主库没死,只是“忙到没空理你”。切换过去后,老主库恢复正常当上了从库,然后全量同步把一份新数据拉过来,看起来没啥,但那段卡顿时间里的写请求可能已经丢了。
这类问题排查思路分三步:
- 查是否真的宕机:看 Redis 日志有没有 OOM、致命的 segfault;看系统负载和 CPU 是否被打满。
- 查是否有慢命令:用
SLOWLOG GET看最近的慢查询。 - 调整误判阈值:
down-after-milliseconds从 5000 调到 10000,给主从切换留出缓冲。
脑裂我前面已经在哨兵部分详细说了,核心防护是开启min-replicas-to-write。集群模式下也有类似的隐患。比如某个主节点和集群内其他节点网络隔离,它还在持续接收客户端写入,其他节点等cluster-node-timeout过后就会把它的从节点提升为新主节点。等网络恢复,老主节点发现自己被“降级”,会自动丢弃自己那些没有被复制走的写入数据。集群模式下没有类似min-replicas-to-write的全局保护参数,所以最靠谱的方案还是业务侧做好幂等,或者必要的时候在接入层做降级限流。
数据丢失的另一种常见来源是持久化配置不对。高可用架构虽然能帮你切换节点,但切换后的新主节点靠的是复制数据。如果你连appendonly yes都没开,或者用了默认的 RDB 策略,极端情况下数据可能根本没写到磁盘。我的建议是:高可用模式下必须开 AOF,并且配置appendfsync everysec。这个配置能够在性能和持久化之间取得一个比较平衡的状态,最多丢一秒数据,对于大多数业务是可以接受的。always性能损耗太大,no又太不安全。
5.2 集群槽位、网络与客户端问题
集群排查中,槽位问题是重灾区。
见过不止一次有人连集群中的某个节点执行redis-cli -h 10.0.0.2 -p 6379 get foo,得到一个MOVED 6275 10.0.0.5:6379的错误。这不是故障,而是集群的正常行为——你的请求打到了不负责该槽位的节点上。解决方案就是加-c参数,或者让客户端使用集群模式连接。
还有一种常见坑是槽位指派不完整。比如你用了cluster replicate手动设主从,却忘了给主节点分配槽位,结果cluster info显示cluster_state:fail,因为当前集群存在未覆盖的槽位。排查方法:
redis-cli -p 6379 cluster nodes | awk '{print $9}' | grep '\\-'这会列出所有没有被分配的槽位区间。如果确实有,用cluster addslots逐个补上,或者直接用--cluster fix自动修复。
槽位迁移卡住也是高频问题。症状是cluster_state:ok,但某些 key 访问时一直报ASK,重定向过去又找不到数据。大概率是迁移中间态没走完。排查命令:
redis-cli -p 6379 cluster slots找到状态异常的槽位后,重新执行redis-cli --cluster fix来修复迁移状态。注意,如果迁移的 key 数量特别大,槽位迁移耗时十几分钟是正常的,不要一看到ASK就慌着手动干预。
网络层面,集群对端口的依赖比哨兵更苛刻。开防火墙时除了 6379 这个数据端口,还一定要放通 16379 这个集群总线端口。我遇到过最隐蔽的一次故障:6379 通了,16379 被安全组沉默拒绝,节点之间 PING 丢包严重,集群状态在ok和fail之间反复横跳,客户端疯狂报CLUSTERDOWN。当时排查了很久才发现是这个端口被拒了。
客户端这块,最容易出问题的是“连接池里的旧路由”。虽然客户端有集群拓扑刷新机制,但刷新是有周期的。如果你的连接池缓存了旧节点,扩容或摘节点后可能会出现短时间的连接异常。解决办法是在低峰期执行扩容,并且把客户端的拓扑刷新时间调小,比如 Lettuce 的cluster topology refresh周期默认是 1 分钟,线上改成 10 秒会更敏感。
5.3 缓存治理与分布式锁的高可用边界
聊高可用,就不能只谈机器层面,业务用的缓存治理和分布式锁也是高可用架构的一部分。很多团队把 Redis 高可用搭好了,但业务代码写得非常粗糙,一样会出大事。
先说缓存治理,三个高频词你肯定听过:穿透、击穿、雪崩。
- 穿透:查询一个不存在的 key,请求直接打到数据库。防护手段是布隆过滤器,或者在空结果上也设置短 TTL 缓存。
- 击穿:某个热点 key 在过期瞬间有大量请求同时打到数据库。防护手段是互斥锁重建缓存,或者把过期时间做成逻辑过期,即业务值里存一个过期标记,后台异步刷新。
- 雪崩:大量 key 在同一时间过期,或者 Redis 整体挂了。防护手段是过期时间加随机抖动,以及在上层做限流降级。
我在实际项目里最认可的“缓存高可用治理”顺序是:先保证 Redis 本身的可用性(哨兵/集群),再在业务代码里实现多级缓存(本地缓存 + Redis + DB),最后再加限流和降级。三层配合,才能扛住真正的高并发。
再说分布式锁。Redis 分布式锁最经典的实现是 SETNX + EXPIRE 的原子操作,核心命令:
SET lock_key request_id NX PX 30000必须用 SET 命令的 NX 和 PX 一起传,保证加锁和过期在一个原子操作里。千万不要先 SETNX 成功,再单独执行 EXPIRE,这两个步骤之间进程崩溃会导致锁永远不释放。
锁的释放也要求校验值。释放时需要比对 value 是否还是自己的 request_id,然后Lua脚本原子删除:
if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end很多人写释放锁代码的时候直接 DEL,结果把别人的锁删了,这在并发高的场景下会造成严重的互斥失效。
说到高可用,分布式锁的安全争议集中在 Redlock 算法:官方提出的用多个独立 Redis 实例组锁的方案。我的看法是,普通业务用单主 Redis + 哨兵模式做加锁是够用的;如果你们的业务真的非常依赖分布式锁的强一致语义,用 Redlock 需要额外部署多个 Redis 实例,且要接受它实现复杂、性能损耗大的现实。真要较真,更好的方案是转到 ZooKeeper 或 etcd 这类带线性一致读写的组件上去。别把 Redis 当万能锁用,它更适合做“高性能非强一致”的互斥控制。
最后再分享一个小技巧。我在压测哨兵切换时,不只看 Redis 是否恢复,还会盯“切换耗时”这个指标。从哨兵判定 ODOWN 到客户端连接新 Master 完成,整个过程最好控制在 30 秒以内。超出这个时间就要查是不是parallel-syncs配置过大、慢查询阻塞了新主库的复制,或者客户端对 switch-master 事件的处理逻辑有问题。高可用这事,不是“能切就行”,而是“切换得够快、够稳、不漏数据”。这套标准,才是你在团队里真正值钱的经验。