Redis 这套东西,刚上手的时候觉得就是个内存缓存,set/get 一把梭,能有多难?可真到了生产环境,尤其是流量一上来、机器一重启、主从一切换,各种幺蛾子全冒出来了。我见过太多团队,Redis 用着用着就变成了"单点定时炸弹"——没做高可用,主节点一挂整个业务雪崩;没配持久化,重启之后缓存全空,数据库瞬间被击穿;性能不管理,慢查询堆着堆着就把内存打爆了。这篇就围绕Redis 高可用、持久化及性能管理这三块,把我这些年踩过的坑、调过的参数、验证过的方案,从头到尾捋一遍。不管你是刚接触 Redis 的新手,还是已经管着几十个实例的老手,应该都能从里面找到能直接抄作业的东西。
1. 先把整体思路理清楚:为什么这三件事必须一起做
1.1 高可用、持久化、性能管理不是三个独立话题
很多人学 Redis 是割裂着学的:今天看主从复制,明天看 RDB 和 AOF,后天调 maxmemory。结果真到线上出问题,发现这三者是互相咬合的。举个最典型的场景:主节点宕机,哨兵把从节点提升为主——这个"高可用"动作能不能顺利完成,取决于从节点的数据是不是足够新,而数据新不新,直接跟持久化和复制积压缓冲区挂钩。再比如,你为了性能把 RDB 的 save 频率调得很低,结果宕机时丢了一大段数据,高可用切换过去之后数据是残缺的,业务照样出问题。
所以我的建议是,把这三块当成一个整体来设计。高可用解决的是"服务别断",持久化解决的是"数据别丢",性能管理解决的是"别把自己撑死"。三者优先级怎么排,取决于你的业务:如果是纯缓存场景,丢了能回源重建,那持久化可以弱化,重点保高可用和性能;如果是拿 Redis 当存储用(比如做排行榜、计数器、分布式锁),那持久化就是命根子,必须重点保障。
我一般会先问自己三个问题:这份数据丢了能不能从别处恢复?服务中断几分钟业务能不能忍?内存和 QPS 的天花板在哪?把这三个问题回答清楚,方案基本就定型了。
1.2 三种部署形态的取舍逻辑
Redis 的部署形态,从简单到复杂大致是:单机、主从、哨兵、集群。选哪个不是拍脑袋,得看数据量和可用性要求。
单机就不用说了,只适合开发测试,生产环境上单机基本等于给自己埋雷。主从复制解决了读扩展和数据备份的问题,但主挂了需要人工介入切换,做不到自动故障转移。哨兵在主观下线、客观下线、领导者选举这一套机制下,能实现自动切换,是中小规模场景下性价比最高的高可用方案。集群则是为了解决单机内存上限和写扩展问题,把数据分片到多个主节点上,每个分片再配从节点。
这里有个常见的误区:以为集群就一定比哨兵高级。其实不是。集群的运维复杂度、客户端兼容性要求、跨槽操作的限制,都是实打实的成本。如果你的数据量没超过单机内存上限(比如 32G 以内),QPS 也没到单节点扛不住的程度,哨兵方案往往更省心。我见过有团队数据才几个 G,硬上集群,结果为了处理跨槽的 mget 问题折腾了好几周,纯属自找麻烦。
1.3 一个我常用的决策参考表
| 场景特征 | 推荐形态 | 持久化策略 | 关键理由 |
|---|---|---|---|
| 开发测试、数据可丢 | 单机 | 可关闭 | 省资源,快速迭代 |
| 读多写少、数据量小 | 主从 + 哨兵 | RDB 为主 | 自动切换,运维简单 |
| 数据重要、不能丢 | 主从 + 哨兵 | AOF everysec | 兼顾安全与性能 |
| 数据量大、写并发高 | 集群 | RDB + AOF | 分片扩展,双重保障 |
| 纯缓存、可回源 | 主从 + 哨兵 | 可弱化 | 优先保性能和可用 |
这张表不是死的,实际选型还要结合团队运维能力。一个只有两三个人的小团队,硬上集群,出了问题没人能快速定位,反而更危险。
2. 高可用方案落地:从主从复制到哨兵自动切换
2.1 主从复制的完整握手过程
主从复制是 Redis 高可用的地基,搞不清楚它的握手流程,后面排查问题就是抓瞎。整个复制过程分几个阶段:
第一阶段是建立连接、协商同步。从节点执行replicaof命令后,会跟主节点建立 socket 连接,然后发送 PING 确认对方活着,接着进行身份验证(如果配了 requirepass),再发送自己的监听端口信息。这一步做完,从节点会发送PSYNC命令,带上自己记录的 runid 和复制偏移量。
第二阶段是全量同步或部分同步。主节点收到 PSYNC 后判断:如果从节点带的 runid 跟自己不一致,或者偏移量已经不在复制积压缓冲区里了,就触发全量同步(full resync);否则走部分同步(continue),只补发缺失的那部分数据。全量同步的过程是:主节点 fork 一个子进程生成 RDB 快照,同时把 fork 之后的新写命令攒在缓冲区里,RDB 生成完发给从节点,从节点先清空自己的数据再加载 RDB,最后主节点把缓冲区里的增量命令补发过去。
第三阶段是命令传播。全量同步完成后,主节点每执行一条写命令,就异步发给所有从节点,从节点执行同样的命令保持数据一致。同时从节点会周期性发送REPLCONF ACK上报自己的偏移量,主节点据此判断从节点是否掉队。
提示:
repl-backlog-size这个参数非常关键,它决定了部分同步能不能成功。默认 1MB,在高写入场景下很容易被写满,导致从节点重连时被迫全量同步。我一般会按"平均写入速率 × 断连容忍秒数"来估算,比如每秒写 2MB、容忍断 60 秒,那就设成 128MB 比较稳妥。
2.2 哨兵机制的核心:三个定时任务和两次下线判断
哨兵(Sentinel)本质是一个独立的进程,它通过三个定时任务来监控整个主从体系:
- 每 10 秒一次:向所有主从节点发送 INFO 命令,获取拓扑结构,发现新的从节点或哨兵。
- 每 2 秒一次:向所有节点的
__sentinel__:hello频道发送自己的信息和判断,同时订阅这个频道,实现哨兵之间互相发现和状态交换。 - 每 1 秒一次:向所有主从节点和其他哨兵发送 PING,做心跳检测。
故障判断分两步。主观下线(SDOWN)是单个哨兵自己的判断:如果 PING 超过down-after-milliseconds没收到有效回复,就认为这个节点挂了。但单个哨兵可能因为网络抖动误判,所以需要客观下线(ODOWN):当认为主节点下线的哨兵数量超过 quorum(法定票数)时,才真正判定主节点客观下线,触发故障转移。
故障转移的流程是:哨兵之间选举出一个领导者(基于 Raft 算法),由领导者从从节点里挑一个最合适的提升为主节点。挑选规则依次是:先过滤掉不健康的从节点,然后比较replica-priority(越小优先级越高),相同则比较复制偏移量(越大数据越新),再相同就比较 runid(越小越优先)。选出来之后,执行slaveof no one提升为主,再让其他从节点指向新主,最后通过发布订阅通知客户端。
2.3 哨兵配置实操与关键参数
下面是一份我常用的哨兵配置模板,可以直接改改就用:
port 26379 daemonize yes logfile "/var/log/redis/sentinel.log" dir "/var/lib/redis/sentinel" # 监控的主节点,quorum 设为 2 sentinel monitor mymaster 192.168.1.10 6379 2 # 主节点多久无响应判定主观下线,单位毫秒 sentinel down-after-milliseconds mymaster 30000 # 故障转移时最多同时有几个从节点同步新主 sentinel parallel-syncs mymaster 1 # 故障转移超时时间 sentinel failover-timeout mymaster 180000 # 主节点密码 sentinel auth-pass mymaster yourpassword几个参数的选择逻辑我解释一下。down-after-milliseconds设 30 秒是经验值,设太短容易误判(比如一次 GC 停顿就触发切换),设太长故障恢复慢。parallel-syncs设 1 是为了避免所有从节点同时向新主发起全量同步,把新主 IO 打满,宁可慢一点也要稳。failover-timeout设 180 秒,给故障转移留足时间,尤其是数据量大的时候全量同步很耗时。
注意:哨兵节点至少部署 3 个,且分布在不同物理机上。如果只部署 2 个,一个挂了另一个无法形成多数派,故障转移就失效了。这是很多人踩过的坑。
2.4 客户端如何正确接入哨兵
哨兵方案里,客户端不能直连主节点 IP,因为主节点会变。正确的做法是客户端连接哨兵集群,通过哨兵获取当前主节点地址。以常见的客户端为例,连接时传入的是哨兵节点列表和 master 名称,客户端内部会订阅哨兵的切换事件,主节点变了自动更新连接。
这里有个容易忽略的点:故障转移期间客户端会短暂报错。因为切换需要时间(通常几秒到几十秒),这期间写请求会失败。业务代码必须做好重试和降级,不能假设 Redis 永远可用。我一般会在客户端配置里加上重试次数和重试间隔,同时业务层对写失败做本地缓存或消息队列缓冲,等 Redis 恢复后再补。
3. 持久化机制:RDB 与 AOF 的取舍和组合
3.1 RDB 的触发时机与 fork 代价
RDB 是把某一时刻的内存数据快照写成二进制文件。触发方式有三种:手动执行SAVE(阻塞主线程,生产禁用)和BGSAVE(fork 子进程,不阻塞主线程)、根据配置自动触发、主从全量同步时自动触发。
自动触发的配置长这样:
save 900 1 # 900 秒内至少 1 个 key 变化 save 300 10 # 300 秒内至少 10 个 key 变化 save 60 10000 # 60 秒内至少 10000 个 key 变化这三个条件是"或"的关系,满足任意一个就触发 BGSAVE。RDB 的优点是文件紧凑、恢复速度快,适合做备份和灾难恢复。缺点是两次快照之间的数据会丢,而且 fork 子进程时如果内存很大,fork 本身会阻塞主线程。
fork 的代价跟内存页表大小成正比。在 Linux 上,fork 采用写时复制(COW),理论上很快,但如果内存有几十 G,页表映射本身就要花几百毫秒甚至更久。我实测过一个 32G 的实例,fork 耗时大概在 300-500ms 之间,这期间主线程是卡住的。所以单实例内存不建议超过 32G,这也是集群分片的一个现实理由。
3.2 AOF 的三种刷盘策略对比
AOF 是把每条写命令追加到日志文件里,恢复时重放这些命令。刷盘策略由appendfsync控制,有三种:
| 策略 | 行为 | 数据安全性 | 性能 |
|---|---|---|---|
| always | 每条命令都 fsync | 最高,几乎不丢 | 最差,QPS 大幅下降 |
| everysec | 每秒 fsync 一次 | 最多丢 1 秒 | 较好,推荐 |
| no | 交给操作系统决定 | 不可控,可能丢很多 | 最好 |
生产环境我基本都用everysec,它在安全性和性能之间取得了很好的平衡。always虽然安全,但每次写都要落盘,QPS 会掉到几千甚至更低,除非是金融级场景否则没必要。no则太不可控,操作系统可能几十秒才刷一次,宕机丢数据量太大。
AOF 文件会随着时间不断膨胀,所以需要重写(rewrite)来压缩。重写也是 fork 子进程完成的,子进程根据当前内存数据生成一份最精简的命令集,比如一个 key 被 set 了 100 次,重写后只保留最后一次。重写期间的新命令会同时写入旧 AOF 缓冲和新 AOF 缓冲,重写完成后合并。
auto-aof-rewrite-percentage 100 # 文件比上次重写后增长 100% 触发 auto-aof-rewrite-min-size 64mb # 文件至少 64MB 才触发3.3 混合持久化:兼顾恢复速度和数据安全
Redis 4.0 之后引入了混合持久化,配置aof-use-rdb-preamble yes开启。它的原理是:AOF 重写时,先把当前内存数据以 RDB 格式写入 AOF 文件的开头,之后的增量命令再以 AOF 格式追加。这样恢复时先加载 RDB 部分(快),再重放 AOF 部分(补全),兼顾了 RDB 的恢复速度和 AOF 的数据安全。
我现在的默认配置就是混合持久化 + everysec,实测下来恢复一个 10G 的实例,纯 AOF 要几分钟,混合持久化只要几十秒,差距非常明显。
3.4 持久化选型的实操建议
到底怎么选,我给几个具体场景的建议:
- 纯缓存场景:可以只开 RDB,甚至关闭持久化。因为数据能从数据库回源,丢了无所谓,重点是别让持久化拖累性能。
- 数据不能丢但能容忍秒级损失:AOF everysec + 混合持久化,这是最通用的方案。
- 数据绝对不能丢:AOF always,但要接受性能下降,同时做好主从和哨兵。
- 需要快速恢复的大实例:RDB 为主 + AOF 兜底,或者混合持久化。
提示:无论选哪种,都要定期验证备份文件的可用性。我见过太多团队配了持久化,结果真出事时发现 RDB 文件损坏或者 AOF 文件不完整,白配了。建议定期做恢复演练,把备份文件拉到测试环境实际恢复一次。
4. 性能管理:内存、慢查询与连接数调优
4.1 内存管理:maxmemory 与淘汰策略
Redis 是内存数据库,内存管理是性能的生命线。核心参数是maxmemory,它限制 Redis 能用的最大内存。如果不设,Redis 会一直吃内存直到把机器撑爆,触发 OOM Killer,进程被杀,这比主动淘汰还糟糕。
maxmemory 8gb maxmemory-policy allkeys-lru淘汰策略有八种,常用的几种:
- noeviction:不淘汰,写入报错。适合当存储用、数据不能丢的场景。
- allkeys-lru:所有 key 里淘汰最近最少使用的。适合纯缓存。
- volatile-lru:只淘汰设了过期时间的 key。适合缓存和持久数据混存的场景。
- allkeys-lfu / volatile-lfu:按访问频率淘汰,比 LRU 更精准,适合热点数据明显的场景。
选策略的逻辑:如果所有数据都是缓存,用 allkeys-lru;如果一部分是缓存一部分是持久数据,用 volatile-lru,并给缓存 key 都设过期时间;如果热点集中,用 lfu 系列。
这里有个坑:淘汰是采样近似算法,不是精确的。maxmemory-samples控制采样数量,默认 5,调大更精确但更耗 CPU。一般 5-10 就够了,不用调太高。
4.2 慢查询日志:定位性能瓶颈的利器
Redis 的慢查询日志记录执行时间超过阈值的命令。配置两个参数:
slowlog-log-slower-than 10000 # 超过 10000 微秒(10ms)记录 slowlog-max-len 128 # 最多保留 128 条slowlog-log-slower-than设 10ms 是个合理的起点。注意单位是微秒,别设错了。设 0 会记录所有命令,性能影响大;设负数则关闭。
查看慢查询用SLOWLOG GET,会返回命令、执行时间、时间戳等信息。我排查性能问题时,第一步就是看慢查询日志,往往能直接定位到问题命令。常见的慢命令有:keys *(全量扫描,生产禁用)、hgetall大 hash、smembers大 set、sort大列表、zrange大范围等。
注意:慢查询日志只记录命令执行时间,不包括网络传输和排队时间。所以如果慢查询日志里没有慢命令,但客户端还是感觉慢,那问题可能出在网络、连接数或者阻塞命令上。
4.3 连接数与客户端缓冲区管理
连接数方面,maxclients控制最大客户端连接数,默认 10000。如果连接数打满,新连接会被拒绝。排查连接泄漏要看CLIENT LIST,找出空闲连接和异常连接。
客户端缓冲区是另一个容易被忽视的点。Redis 为每个客户端维护输入缓冲和输出缓冲,如果客户端消费慢,输出缓冲会不断增长,吃光内存。相关参数:
client-output-buffer-limit normal 0 0 0 client-output-buffer-limit replica 256mb 64mb 60 client-output-buffer-limit pubsub 32mb 8mb 60格式是"硬限制 软限制 软限制持续秒数"。超过硬限制立即断开,超过软限制且持续超过指定秒数则断开。对于从节点和发布订阅客户端,一定要设限制,否则一个慢从节点就能把主节点内存拖垮。
4.4 大 key 和热 key 的治理
大 key 是性能杀手。一个几 MB 的 hash 或 list,操作一次就可能阻塞主线程几百毫秒。治理方法:一是拆分,把大 key 拆成多个小 key,比如按 hash 字段分片;二是用SCAN系列命令渐进式遍历,替代keys和hgetall;三是定期用redis-cli --bigkeys扫描发现大 key。
热 key 则是访问过于集中,单个 key 的 QPS 过高,可能打满单个分片。治理方法:一是本地缓存,在客户端缓存热 key;二是读写分离,把读请求分散到多个从节点;三是 key 拆分,给热 key 加随机后缀分散到多个 key 上。
5. 常见问题排查与避坑经验实录
5.1 故障转移后数据丢失怎么办
这是哨兵方案最常被问到的问题。原因通常是:主节点宕机时,有一部分写命令还没同步到从节点,切换后这部分数据就丢了。Redis 的复制是异步的,这个窗口无法完全消除,只能缩小。
缓解手段:一是调小repl-backlog-size相关的断连容忍时间,让从节点尽量跟上;二是用min-replicas-to-write和min-replicas-max-lag参数,强制主节点在有足够健康从节点时才接受写入,牺牲可用性换一致性。
min-replicas-to-write 1 min-replicas-max-lag 10意思是至少 1 个从节点延迟不超过 10 秒,主节点才接受写。这样能大幅减少切换时的数据丢失,但代价是如果从节点都挂了,主节点会拒绝写入。
5.2 内存突然暴涨的排查思路
内存暴涨一般有几个原因:大 key 写入、客户端输出缓冲堆积、复制积压缓冲过大、碎片率过高。排查步骤:
INFO memory看 used_memory、used_memory_rss、mem_fragmentation_ratio。- 如果碎片率超过 1.5,考虑开启 activedefrag 或重启实例。
CLIENT LIST看有没有输出缓冲异常的客户端。redis-cli --bigkeys扫描大 key。- 检查是否有大量 key 没设过期时间,导致只增不减。
5.3 常见问题速查表
| 现象 | 可能原因 | 排查命令 | 解决方向 |
|---|---|---|---|
| 主从切换后数据缺失 | 异步复制窗口 | INFO replication | 配置 min-replicas |
| 内存持续增长 | 大 key / 无过期 | INFO memory, --bigkeys | 拆分 key,设过期 |
| 响应变慢 | 慢命令 / 阻塞 | SLOWLOG GET | 优化命令,禁用 keys |
| 连接被拒 | 连接数打满 | CLIENT LIST | 调大 maxclients,查泄漏 |
| 从节点频繁全量同步 | backlog 太小 | INFO stats | 调大 repl-backlog-size |
| fork 卡顿 | 内存过大 | LATENCY DOCTOR | 控制单实例内存 |
5.4 我踩过的几个真实坑
第一个坑是持久化文件把磁盘写满。AOF 文件增长很快,如果磁盘没监控,写满之后 Redis 会拒绝写入。后来我加了磁盘告警,并且把 AOF 重写阈值调低,让它更频繁地压缩。
第二个坑是哨兵和主从配置不一致。有一次主节点设了密码,哨兵配置里忘了加sentinel auth-pass,导致哨兵无法监控,故障转移失效。配置变更一定要同步到所有相关组件。
第三个坑是客户端没处理重连。故障转移期间连接断开,客户端没做重试,业务直接报错。后来在客户端加了连接池和重试逻辑,才稳定下来。
第四个坑是大 key 导致的阻塞。有个业务往一个 list 里塞了几十万条数据,一次lrange就卡住主线程。后来改成用lrange分批取,并且限制 list 长度,问题才解决。
5.5 监控指标清单
最后给一份我常用的监控指标清单,这些指标建议都接入告警:
- 可用性:主从状态、哨兵数量、故障转移次数。
- 性能:QPS、命中率、慢查询数量、平均响应时间。
- 内存:used_memory、碎片率、淘汰 key 数量。
- 持久化:RDB 最后保存时间、AOF 文件大小、重写次数。
- 连接:当前连接数、拒绝连接数、输出缓冲大小。
- 复制:主从偏移量差、全量同步次数。
命中率低于 90% 就要警惕了,可能是缓存穿透或者 key 设计有问题。淘汰 key 数量持续增长,说明内存不够用,要么扩容要么优化数据结构。
这套东西说起来多,但真正落地之后,Redis 的稳定性会有质的提升。我个人的体会是,高可用、持久化、性能管理这三块,任何一块偷懒,最后都会以线上故障的形式还回来。与其出事之后救火,不如一开始就把配置做扎实,把监控做全,把演练做够。