写点什么

Java 王者修炼手册【Redis 篇 - 高可用底层机制】:吃透持久化 / 内存淘汰策略 / 过期淘汰策略,掌控 Redis 高可用

作者:DonaldCen
  • 2025-12-13
    广东
  • 本文字数:6299 字

    阅读完需:约 21 分钟

Java 王者修炼手册【Redis 篇 - 高可用底层机制】:吃透持久化 / 内存淘汰策略 / 过期淘汰策略,掌控 Redis 高可用

大家好,我是程序员强子。


今天来练习场学习 Redis 高可用的基石~


Redis 高可用的核心是服务持续可用数据不丢失

  • 持久化:是数据安全基石,防宕机数据丢失,支撑主从同步,保障单实例 / 集群 恢复后数据完整

  • 内存淘汰策略:避免内存溢出导致 Redis 进程被杀死,维持服务稳定;

  • 过期淘汰策略:清理无效过期数据,减少内存占用,避免资源浪费影响服务可用性

赶快上车,准备发车了~

持久化机制

RDB

定义

Redis 的快照式持久化机制:

  • 通过生成某个时间点的内存数据全量二进制快照文件(.rdb)

  • 将数据以紧凑的二进制格式持久化到磁盘

  • 重启时可通过加载该文件恢复数据

触发机制

分为手动触发和自动触发

  • 手动触发 save 命令 bgsave 命令

  • 自动触发 save m n 配置主从复制触发 shutdown/flushall 触发

它们底层都是依赖 fork()系统调用与写时复制(COW)机制

手动触发-save 命令

同步触发Redis 主线程直接执行快照生成

主线程被阻塞,无法处理任何客户端的读写请求,生产环境严禁使用!!!

执行流程如下:



为什么这么设计?

因为 save 是主线程直接操作,若同时处理写请求,会导致快照数据不一致


手动触发-bgsave 命令

bgsave 是异步快照,是生产环境的主流方式


核心依赖 fork()系统调用 +写时复制(COW) 机制


既保证快照一致性,又允许主线程处理写请求


步骤如下:

  • 主线程执行 fork():创建子进程,此时父子进程共享内存页表(注意:不是复制实际数据,只是共享内存地址的映射,耗时微秒级)。

  • 子进程生成快照:子进程读取内存中的数据,写入 RDB 文件,这个过程中子进程只读不写。

  • 主线程正常接收客户端的 set、hset 等写命令,当要修改某块内存数据时,Redis 会触发写时复制—— 复制该数据所在的内存页(生成新的内存页),主线程修改新页的数据,而子进程依然读取旧页的数据完成快照



执行快照时,数据能被修改吗?

bgsave 期间,数据可以正常修改,且快照捕获的是 fork()时刻的内存数据(一致性快照),后续主线程的修改不会影响快照内容

虽然数据能修改,但是有副作用

  • 内存膨胀:COW 期间,修改过的内存页会生成新页,导致 Redis 内存使用量临时翻倍(比如原本 10GB 内存,可能涨到 20GB),若服务器内存不足,可能触发 OOM

  • fork()短暂阻塞:fork()本身会占用主线程少量时间(与内存大小正相关,比如 20GB 内存可能阻塞 100ms+),期间无法处理请求

自动触发-save m n 配置

  • 当 m 秒内有 n 次键值修改时,自动触发 bgsave

  • 如 save 60 1000 表示 60 秒内 1000 次修改则触发

  • 多个 save 配置是或关系(满足任一即触发),默认配置:save 900 1、save 300 10、save 60 10000

主从复制触发

主节点接收到从节点的同步请求时,会自动执行 bgsave 生成 RDB 文件并发送给从节点

shutdown/flushall 触发

  • 会先执行 save 生成 RDB ,是 save 而不是 bgsave

  • 因为准备关闭了,就不再接受任何其他请求,所以同步阻塞刚好完美合适。

  • 而如果是 bgsave 的话,是异步的,创建子进程同步,那主进程挂掉,子进程却没有关闭,显然不合适。

使用场景

冷备份与灾难恢复

  • 配置定时任务(如 Linux 的 crontab),每天凌晨 3 点(业务低峰期)执行 redis-cli bgsave 生成 RDB 文件

  • 将生成的 RDB 文件通过 rsync/ssh 同步到异地存储

  • 当 Redis 实例宕机且无法通过主从恢复时,将异地备份的 RDB 文件拷贝到新服务器,启动 Redis 加载 RDB 恢复数据

Redis 主从复制的初始同步

  • Redis 主从架构中,从节点首次连接主节点时,会触发全量同步

  • 从节点发送 psync 命令给主节点,请求同步数据

  • 主节点执行 bgsave 生成 RDB 文件,同时将后续的写命令记录到复制缓冲区(相当于增量)

  • 主节点将 RDB 文件发送给从节点,从节点加载 RDB 文件完成数据初始化

  • 主节点将复制缓冲区的写命令发送给从节点,从节点执行后,主从数据一致

大内存 Redis 实例的重启恢复

当 Redis 实例内存较大(如 10GB 以上),重启时加载 RDB 文件的速度远快于 AOF 日志重放

  • 生产环境中,大内存实例若只开启 AOF,重启时间过长会导致服务不可用,而 RDB 能大幅缩短恢复时间

  • 10GB 数据:加载 RDB 文件可能只需要 10 秒,而 AOF 日志重放可能需要 100 秒以上

非核心业务的缓存持久化

对于非核心业务(如商品缓存、文章阅读量计数、用户行为统计),可以仅用 RDB 做持久化,牺牲少量数据安全性换取性能

  • 设置宽松的 save 规则(如 save 300 10,5 分钟 10 次修改触发),减少 bgsave 的频率,降低 IO 开销

跨环境数据迁移

在开发 / 测试过程中,经常需要将生产环境的 Redis 数据同步到测试环境

  • 在生产环境执行 redis-cli bgsave 生成 RDB 文件

  • 将 RDB 文件拷贝到测试环境的 Redis 数据目录

  • 重启测试环境的 Redis,加载 RDB 文件,瞬间同步生产数据

混合持久化的基础

Redis4.0 引入了 RDB+AOF 混合持久化,将 RDB 的优势和 AOF 的优势结合

  • 原理:AOF 文件的开头是 RDB 的全量快照,后面是增量的 AOF 日志(记录快照后的写命令);

  • 落地方式:配置 aof-use-rdb-preamble yes,此时 AOF 文件兼具 RDB 的恢复速度和 AOF 的数据安全性;

  • 本质:混合持久化中的全量部分依然是 RDB,可见 RDB 是 Redis 持久化的底层基础

RDB 优缺点

优势

  • 恢复速度极快:二进制文件加载效率远高于 AOF 日志重放,适合大内存 Redis 实例重启

  • 文件紧凑:全量快照压缩比高,占用磁盘空间小(适合备份存储);

  • 低性能影响:bgsave 子进程独立工作,主线程仅在 fork()时短暂阻塞

劣势

  • 数据丢失风险:仅保存最近一次快照后的修改,若快照后宕机,期间数据全部丢失(如 save 60 1000 配置下,最多丢失 60 秒数据);

  • fork()阻塞风险:大内存实例 fork()耗时较长(如 20GB 内存可能阻塞 100ms+),影响服务可用性;

  • 内存膨胀风险:COW 期间大量写操作会导致内存使用翻倍,可能触发 OOM;

  • 不适合实时持久化:无法做到秒级数据持久化(依赖快照触发规则)

AOF

定义

追加式日志持久化机制

  • 它不会存储数据的快照,而是将 Redis 执行的所有写命令(如 set、hset、lpush 等)以 Redis 协议格式追加到磁盘的 AOF 文件中。

  • 当 Redis 重启时,会重新执行 AOF 文件中的所有命令,从而恢复数据

工作流程

第一阶段:命令写入(Append)

当 Redis 执行一个写命令时,会先将命令写入 AOF 缓冲区(而非直接写入磁盘),再返回客户端 执行成功

为什么要先写缓冲区?

  • 磁盘 IO 是慢速操作,若每次写命令都直接刷盘,会导致 Redis 性能急剧下降(每秒仅能处理数千个请求);

  • 缓冲区可以批量写入磁盘,减少 IO 次数,提升性能

  • 写缓冲设计思想,Java 中的 BufferedWriter 也是同理

第二阶段:缓冲区同步到磁盘(Sync)

Redis 会根据配置的同步策略,将 AOF 缓冲区中的命令刷写到磁盘的 AOF 文件中

第三阶段:AOF 文件重写(Rewrite)

  • 如果一直追加命令,AOF 文件会越来越大

  • 比如执行 10 万次 incr counter,AOF 会记录 10 万条命令,

  • 不仅占用磁盘空间,还会导致 Redis 重启时恢复数据的时间大幅增加

AOF 重写的核心作用是什么?

  • 生成一个新的 AOF 文件,用最少的命令表示当前的数据集

  • 比如 10 万次 incr 会被重写为 set counter 100000,从而压缩文件体积

那重写的触发方式是怎么样的呢?

  • 手动触发:执行 bgrewriteaof 命令(推荐,生产常用);

  • 自动触发:满足配置文件中的重写阈值(如文件体积增长到原来的 100% 且超过 64MB)

重写底层流程

  • 主线程执行 fork()创建子进程,子进程负责重写 AOF 文件

  • 主线程继续处理客户端请求,新的写命令会同时追加到旧 AOF 缓冲区AOF 重写缓冲区(保证重写期间的命令不丢失);

  • 子进程遍历内存数据,生成新的 AOF 文件(仅包含最终数据的命令);

  • 子进程完成重写后,主线程将 AOF 重写缓冲区中的命令追加到新 AOF 文件

  • 主线程将旧 AOF 文件替换为新 AOF 文件,完成重写


为什么重写不会读旧 AOF 文件?

  • 很多同学误以为重写是对旧 AOF 文件的 压缩编辑,实际上子进程直接遍历内存数据生成新命令

  • 旧 AOF 文件仅作为备份,直到重写完成后被替换。

  • 这一设计避免了读取大文件的 IO 开销

生产配置

redis.conf

# 开启AOF持久化(默认关闭,需手动开启)appendonly yes
# AOF文件名称(默认appendonly.aof)appendfilename "appendonly.aof"
# AOF文件存储路径(与RDB相同,默认是Redis工作目录)dir /var/lib/redis
# 同步策略(生产推荐everysec)appendfsync everysec# appendfsync always# appendfsync no
# 重写期间是否停止同步(生产推荐no,保证数据安全)no-appendfsync-on-rewrite no
# 自动重写阈值:文件体积增长超过100%,且文件大小超过64MB时触发auto-aof-rewrite-percentage 100auto-aof-rewrite-min-size 64mb
# AOF文件加载时,若发现损坏是否继续(生产推荐yes,可后续修复)aof-load-truncated yes
# 开启AOF与RDB混合持久化(Redis4.0+,生产推荐开启)aof-use-rdb-preamble yes
复制代码

优缺点

优势

  • 数据安全性高:可配置 everysec 实现秒级数据持久化,最多丢失 1 秒数据;若配置 always,可实现数据零丢失(牺牲性能);

  • 文件可读性强:明文协议格式,可手动编辑、修复,甚至通过 cat 命令直接查看命令记录;

  • 增量持久化:仅记录写命令,写入开销小(相比 RDB 的全量快照);

  • 重写机制优化:通过重写压缩文件体积,避免文件无限膨胀

劣势

  • 恢复速度慢:重启时需要重放所有命令,大文件恢复时间远长于 RDB(如 10GB 数据,AOF 恢复可能需要几分钟,RDB 仅需十几秒);

  • 文件体积大:即使经过重写,AOF 文件体积通常也比 RDB 大 2~3 倍(因为存储的是命令,而非二进制数据);

  • 写性能影响略大:虽然有缓冲区,但同步策略会带来一定的 IO 开销(everysec 的性能影响约为 RDB 的 1.5 倍);

  • 重写时的内存风险:fork()创建子进程时,若内存较大,会有短暂阻塞;重写期间,新命令会写入两个缓冲区,可能导致内存临时上涨

AOF+RDB 混合持久化

混合持久化的原理

  • AOF 文件结构变化:新的 AOF 文件开头是 RDB 的二进制快照,后面是增量的 AOF 命令日志(记录快照后的写命令)

  • 恢复流程:Redis 重启时,先加载 RDB 快照(快速恢复大部分数据),再重放后续的 AOF 命令(恢复增量数据)

核心优势

  • 完美弥补 AOP 和 RDB 的短板

  • 恢复速度快:比纯 AOF 快 10 倍以上,接近 RDB 的恢复速度;

  • 数据安全性高:增量部分用 AOF 记录,最多丢失 1 秒数据;

  • 文件体积小:开头的 RDB 快照压缩比高,整体体积比纯 AOF 小

# 开启混合持久化(默认关闭,需手动开启)aof-use-rdb-preamble yes
复制代码

过期淘汰策略

仅针对显式设置了 expire 过期时间的 key,解决 过期 key 长期占用内存 的问题,属于 过期数据的清理机制

惰性删除

原理

  • 只有当客户端主动访问某个 key 时,Redis 才会检查该 key 是否过期

  • 若过期则立即删除,返回 nil;

  • 若未过期则正常返回数据

优点

极致节省 CPU 资源(无需主动扫描),仅在必要时执行删除操作。

缺点

  • 可能导致严重内存泄漏

  • 若过期 key 长期不被访问,会一直占用内存

  • 比如缓存的冷数据过期后无人访问

底层实现

  • Redis 在 db.c 的 lookupKey 函数中嵌入过期检查逻辑,属于同步操作

  • 耗时极短,不会阻塞主线程

定期删除

原理

  • Redis 主线程每隔固定时间(默认 100ms,由 hz 参数控制,默认 hz=10 即每秒执行 10 次),随机抽取部分过期 key(默认抽样数为 maxmemory-samples=5)进行检查,删除其中已过期的 key

  • 每次执行时,Redis 会限制耗时(默认不超过 CPU 时间的 25%),避免阻塞主线程;

  • 若一次抽样中过期 key 比例超过 25%,会继续抽样删除,直到比例低于 25%达到时间限制

优点

平衡 CPU 和内存,既避免惰性删除的内存泄漏,又避免全量扫描的 CPU 消耗。

调优建议

  • 高并发场景(如秒杀):可将 hz 提高到 20-50(代价是增加 CPU 消耗,建议不超过 50);

  • 低并发 / 内存敏感场景:保持 hz=10 即可

主动淘汰

当 Redis 触发内存淘汰策略时,会优先从过期 key 中选择淘汰对象(比如 volatile-lru 策略),属于过期淘汰与内存淘汰的联动机制

辅助规则

  • RDB 持久化:生成 RDB 文件时,会忽略已过期的 key(不会写入 RDB);

  • AOF 持久化:过期 key 被删除时,会记录 DEL 命令到 AOF 文件,重启时通过 AOF 恢复删除逻辑;

  • 主从同步:从库不主动执行过期删除,仅通过主库同步的 DEL 命令删除过期 key

适用场景

  • 读多写少、过期 key 访问频率低依赖惰性删除 + 定期删除(默认配置即可);

  • 内存敏感、冷数据多:提高定期删除的抽样数(maxmemory-samples=10)或 hz 参数;

  • 核心业务数据:避免依赖惰性删除,可通过定时任务主动清理过期 key(如 Redis 的 SCAN 命令批量删除)

内存淘汰策略

生产环境必须显式配置 maxmemory,否则 Redis 默认不限制内存,极易触发 OOM

策略分类

LRULFU 是什么意思呢? 到底有哪些区别呢?跟强子一起研究一下~

LRU & LFU 区别

  • LRU(最近最少使用):基于 **最后访问时间 **判断缺陷是 偶发访问的冷 key 会被误判为热点比如某冷商品被用户偶然点击一次,LRU 会认为它是热点,长期保留

  • LFU(最不经常使用)基于 访问频率 判断每个 key 维护访问计数器,访问时递增,定期衰减比如经常被访问的商品才是真热点

选型步骤

  • 明确数据是否可丢失不可丢失选 noeviction 可丢失选淘汰类

  • 是否区分过期 key 混合存储选 volatile-*纯缓存选 allkeys-*;

  • 根据访问模式选 LRU/LFU/TTL 访问模式复杂选 LFU 热点集中选 LRU 限时数据选 TTL

线上高频问题总结

内存淘汰策略触发后,Redis 如何保证主从一致性?

  • 主库触发淘汰后,会立即将 DEL 命令同步给从库

  • 从库执行 DEL 命令删除对应 key,避免主从数据不一致

大量 key 同时过期为何能引发 Redis 故障?

引发的问题

  • Redis 主线程阻塞,服务响应超时 Redis 所有操作(读 / 写 / 删除)都依赖单主线程执行海量过期 key 的清理操作会抢占主线程资源,导致正常请求被阻塞

  • 缓存雪崩,后端数据库被压垮大量 key 同时过期后,客户端访问这些 key 差不多就去查数据库短时间内数据库连接数被打满,引发数据库宕机

  • 主从同步延迟,数据一致性风险主库集中删除海量过期 key 时,会生成大量 DEL 命令并同步给从库若 DEL 命令数量超过主从同步带宽,会导致从库数据延迟此时从库提供的读服务可能返回已过期的脏数据,甚至引发业务逻辑错误

根本原因分析

  • 惰性删除的 集中触发问题客户端访问 key 时才检查过期并删除若百万级 key 同时过期,且业务侧恰好有大量并发请求访问这些 key 会导致主线程在同一时间集中执行百万次 过期检查 + 删除 key 每个删除操作需要释放 key 的内存、更新哈希表 / 跳表结构,每次操作时间极短,但是百万加起来后也消耗很久期间正常请求无法被处理,直接引发响应超时

  • 定期删除问题 Redis 每隔 100ms 抽样过期 key,若抽样中过期 key 比例 > 25%,会持续抽样删除直到比例达标达到时间限制海量 key 同时过期时,第一次抽样的过期 key 比例可能接近 100%,Redis 会不断执行 抽样删除 循环主线程被持续占用,无法处理正常请求

  • 内存释放问题海量 key 被删除后,Redis 会触发内存碎片整理若 Redis 开启了 AOF 持久化,大量 DEL 命令会被写入 AOF 缓冲区,触发 AOF 刷盘

解决方案

过期时间 打散

给批量 key 的过期时间增加随机偏移

// 伪代码:Java端实现过期时间打散long baseExpire = 24 * 3600; // 基础过期时间(秒)long randomOffset = new Random().nextInt(1200); // 0-20分钟随机偏移jedis.setex(key, baseExpire + randomOffset, value);
复制代码

分层缓存 + 熔断降级

  • 分层缓存:Redis 作为一级缓存,本地缓存(Caffeine/Guaava)作为二级缓存,即使 Redis 缓存失效,本地缓存可承接部分请求;

  • 熔断降级:使用 Sentinel/Hystrix 对数据库访问做熔断当 Redis 返回 nil 的比例超过阈值时,直接返回默认值(如 “优惠券已过期”),避免打穿数据库

总结

Redis 高可用的底层支撑,离不开持久化内存淘汰过期淘汰这三大核心机制

前者守护数据安全,后两者保障资源稳定

吃透三者的协同逻辑,才能真正弄懂 Redis 高可用相关知识点

关注我,后续深挖 Redis 更多底层知识,持续输出硬核干货!

如果觉得帮到你们,烦请 点赞关注推荐 三连,感谢感谢~~

熟练度刷不停,知识点吃透稳,下期接着练~

发布于: 37 分钟前阅读数: 5
用户头像

DonaldCen

关注

有个性,没签名 2019-01-13 加入

跟我在峡谷学Java 公众号:程序员悟空的宝藏乐园

评论

发布
暂无评论
Java 王者修炼手册【Redis 篇 - 高可用底层机制】:吃透持久化 / 内存淘汰策略 / 过期淘汰策略,掌控 Redis 高可用_redis持久化_DonaldCen_InfoQ写作社区