Redis 内存管理与过期淘汰策略:从 maxmemory 到 LRU / LFU,一篇讲透

刚学完 Redis 的内存管理和过期淘汰策略,知识点又多又碎:used_memory 和 used_memory_rss 到底差在哪?为什么 Redis 不用"定时删除"?volatile-lru 和 allkeys-lru 只差一个前缀,效果却天差地别;maxmemory-policy 到底该选哪个?

这篇文章我把内存从哪来 → 怎么管 → 过期怎么删 → 满了怎么淘汰 串成一条线,配图 + 示例 + 面试追问,一次讲清楚。建议收藏,配合 redis-cli 边看边敲。

flowchart TD A[&#34;Redis 内存管理&#34;] --> B[&#34;内存都花在哪?&#34;] A --> C[&#34;怎么查看内存?&#34;] A --> D[&#34;内存碎片怎么处理?&#34;] A --> E[&#34;过期键怎么删除?&#34;] A --> F[&#34;内存满了怎么淘汰?&#34;] B --> B1[&#34;数据本体 + 对象开销<br/>+ 客户端/复制/AOF 缓冲&#34;] C --> C1[&#34;INFO memory<br/>used_memory / rss / frag&#34;] D --> D1[&#34;activedefrag 自动碎片整理&#34;] E --> E1[&#34;惰性删除 + 定期删除&#34;] F --> F1[&#34;maxmemory-policy 全景<br/>LRU / LFU / LRM / 随机 / TTL&#34;] classDef core fill:#D82C20,stroke:#FF6B5E,color:#fff; classDef sub fill:#1E2532,stroke:#2F3949,color:#E6EDF3; class A core; class B,C,D,E,F sub; class B1,C1,D1,E1,F1 sub;

一、先搞懂:Redis 的内存到底花在哪了?

很多人以为"我存了 1GB 数据,Redis 就用 1GB 内存",这是最大的误解。先记住一条底线:

used_memory = Redis 通过自己的分配器(zmalloc)申请到的字节数 ; used_memory_rss = 操作系统看到这个进程真正占着的物理页 。 两者不是包含关系 :used_memory 里的东西未必都在 RSS 里,RSS 里的东西也未必记在 used_memory 上。

flowchart LR subgraph UM[&#34;used_memory:Redis 自己记账的逻辑内存&#34;] direction TB S1[&#34;① 数据本体<br/>(key + value)&#34;] S2[&#34;② 结构开销<br/>redisObject / dictEntry<br/>SDS 头 / expires 字典&#34;] S3[&#34;③ 缓冲区<br/>客户端输入输出缓冲<br/>repl-backlog / AOF buffer&#34;] end S1 --> R0 S2 --> R0 S3 --> R0 R0[&#34;④ 碎片<br/>分配器已释放但没还给 OS 的页&#34;] R2[&#34;⑤ 分配器元数据、代码段与栈、<br/>fork COW 副本页等(不计入 used_memory)&#34;] R0 --> RSS[&#34;used_memory_rss<br/>操作系统视角的物理内存&#34;] R2 --> RSS classDef data fill:#D82C20,stroke:#FF6B5E,color:#fff; classDef onlyrss fill:#F59E0B,stroke:#FFD98A,color:#1A1A1A; classDef other fill:#1E2532,stroke:#2F3949,color:#E6EDF3; class S1 data; class S2,S3 other; class R0,R2 onlyrss; class RSS data;

1.1 数据本身

就是你的 key 和 value。比如 SET name "Tom",这一条数据本身就占几十字节。

1.2 对象结构开销(最容易被忽略)

Redis 不是"裸存字符串",每个 value 都要包一层 redisObject:

c 复制代码
typedef struct redisObject {
    unsigned type:4;       // 类型:string/list/hash/set/zset
    unsigned encoding:4;   // 编码:int/embstr/raw/listpack/quicklist...
    unsigned lru:24;       // LRU 时钟(相对全局 lru_clock 的秒级值)
                           // 或 LFU 数据:高 16 位衰减时间 + 低 8 位计数器
    int refcount;          // 引用计数
    void *ptr;             // 指向实际数据的指针
} robj;

redisObject 在 64 位机器上占 16 字节 (4 字节位域 + 4 字节 refcount + 8 字节指针)。此外,key 要额外存在全局哈希表(dictEntry,约 24 字节)里,每个 key 又有一份 SDS 开销。

💡 举例 :SET a 1 看似只存了 1 个字节,实际上至少是 dictEntry(24B) + key 的 SDS(3~19B 头 + 数据) + redisObject(16B),再加上分配器的 size-class 对齐、哈希表槽位摊销、增量 rehash 期间的临时空间------一条小记录的实际占用往往在 50~100 字节量级 。 所以存很多小 key 时,实际内存往往是"数据体积"的几倍。

1.3 缓冲区

缓冲区 说明
客户端输入缓冲(query buffer) 累积客户端发来的命令,client-query-buffer-limit 兜底
客户端输出缓冲(output buffer) 返回结果的缓冲区,大结果集 / 慢客户端时暴涨
复制缓冲 repl-backlog 主从全量+增量同步用的环形缓冲
AOF 缓冲 写命令先写 AOF buffer,再刷盘

缓冲区上限参数(三档限制,OOM 高发区)

只看类型不够,关键是这几行限制参数------客户端缓冲是 OOM 事件的高发区:

conf 复制代码
# 三档输出缓冲上限,格式:<硬限制> <软限制> <软限制持续秒数>
client-output-buffer-limit normal 0 0 0            # 普通客户端:默认不限(0)
client-output-buffer-limit replica 256mb 64mb 60   # 从库:超 256MB 立即断,或 60s 超 64MB 断
client-output-buffer-limit pubsub 32mb 8mb 60      # 订阅:超限断连
client-query-buffer-limit 1gb                     # 单连接输入缓冲硬上限(7.0+ 可配,默认 1gb)

⚠️ 一个真实事故点 : normal 默认 0 0 0 = 完全不限 :一个慢客户端 + 一次大结果集(比如误用 KEYS *、拉了十万条 Hash 字段),输出缓冲就能吃掉几 GB。注意空闲连接积压的输出缓冲同样计入 used_memory。

1.4 内存碎片

见第三节。


二、怎么查看内存:INFO memory 全字段解读

bash 复制代码
redis-cli info memory

关键字段逐个拆:

字段 含义 怎么看
used_memory Redis 分配器统计的逻辑内存(数据 + 开销 + 缓冲) Redis 自己认为用了多少
used_memory_human 上一项的易读格式 直接看
used_memory_rss 操作系统看这个进程占的物理内存 top / ps 看到的那个值
used_memory_peak 历史峰值 排查"内存突增"用
mem_fragmentation_ratio 碎片率 = rss / used_memory 粗口径,判断整体是否健康
allocator_frag_bytes / allocator_frag_ratio 分配器纯碎片字节 / 比例 排查碎片的精确口径(要 jemalloc)
allocator_rss_bytes / allocator_rss_ratio 分配器持有但进程未使用的页 解释"RSS 里多出来的"到底是什么
maxmemory 配置的容量上限 0 表示不限制
maxmemory_policy 当前淘汰策略 见第五节
mem_allocator 内存分配器,通常 jemalloc 默认 jemalloc
used_memory_dataset used_memory − used_memory_overhead,约等于数据集本身 与 overhead 对比,看开销占比
used_memory_overhead 对象、字典、缓冲区等开销总和 开销过高说明小 key 太多
used_memory_peak_perc 当前 / 历史峰值 的百分比 判断是否长期高位运行
used_memory_startup Redis 启动本身占用 基线参考
used_memory_scripts Lua 脚本占用 脚本缓存泄漏
total_system_memory 操作系统总内存 结合 maxmemory 校验是否超配

💡 判读思路 :used_memory_overhead / used_memory 长期偏高(比如 > 30%),基本可断定存了海量小 key,优化方向是「合并 key / 加大 value 粒度」,而不是调淘汰策略。

2.1 碎片率怎么读?

ini 复制代码
mem_fragmentation_ratio = used_memory_rss / used_memory
比值 含义 处理
1 ~ 1.5 正常 无需处理
> 1.5 碎片较多 看 allocator_frag_bytes 确认,再考虑 activedefrag 或重启
< 1 RSS 比逻辑内存还小 说明发生了 Swap,性能灾难
远大于 1(比如 > 3) 严重碎片 / fork 期间 / 被外部进程占用 排查并整理

💡 为什么 RSS 可能小于 used_memory? 因为操作系统把部分内存 换出到磁盘(swap) 了。逻辑上 Redis 认为自己有这么些内存,但物理上被挪走了。swap 一旦发生,Redis 访问会变成磁盘 IO,延迟直接从微秒飙到毫秒,线上必须避免。

2.2 一个实操例子

bash 复制代码
127.0.0.1:6379> info memory
# Memory
used_memory:1048576            # Redis 认为用了约 1MB
used_memory_human:1.00M
used_memory_rss:1572864        # 系统看到约 1.5MB
mem_fragmentation_ratio:1.50   # 碎片率 1.5,临界
allocator_frag_bytes:131072    # 分配器纯碎片 128KB
maxmemory:1073741824           # 上限 1GB
maxmemory_policy:noeviction    # 默认不淘汰

碎片率 1.5 属于临界,可以先观察,持续升高再开整理。

2.3 排查内存问题的常用命令

INFO memory 是起点,但完整排查还需要下面这些:

命令 / 工具 用途 备注
MEMORY USAGE key 单个 key 实际占用字节数(含对象开销) 定位大 key 的权威 手段,比 STRLEN 准
MEMORY STATS 内存使用全景(各数据集、开销分项) 配合 INFO memory 一起看
MEMORY DOCTOR 内存问题诊断建议 内存涨/降不下来时先跑它
MEMORY PURGE 手动回收碎片(4.0+) 仅 jemalloc 可用;阻塞主线程,低峰期执行
OBJECT FREQ key LFU 访问计数器当前值 必须 maxmemory-policy 为 *-lfu
OBJECT IDLETIME key LRU 空闲秒数 LFU 模式下不可用
OBJECT ENCODING key 查看底层编码 排查编码退化
redis-cli --bigkeys 扫描各类型元素最多的 key 采样 scan,会占 CPU
redis-cli --memkeys 按内存占用排序找大 key(6.0+) 比 --bigkeys 更贴近内存视角
redis-cli --hotkeys 找热 key 依赖 LFU 策略 (*-lfu)
SLOWLOG GET 看慢查询 大 key 删除/淘汰阻塞会在这里露头
bash 复制代码
# 典型排查序列:内存涨了、降不下来
redis-cli memory doctor            # 1. 先问 Redis 自己
redis-cli info memory              # 2. 看 used_memory / rss / 碎片率 / 淘汰策略
redis-cli memory stats             # 3. 看开销分项
redis-cli --memkeys                # 4. 找大 key
redis-cli ttl <sample_key>         # 5. 抽样确认 TTL 没被清掉(返回 -1 就是坑)
redis-cli object freq <hot_key>    # 6. LFU 下看热点是否被正确计数
redis-cli slowlog get 10           # 7. 有没有大 key 导致的慢命令

三、内存碎片:从哪来,怎么治

3.1 碎片是怎么产生的

flowchart LR A[&#34;产生碎片&#34;] --> A1[&#34;① 频繁增删改<br/>内存块反复分配释放&#34;] A --> A2[&#34;② 键值大小跨度大<br/>大块释放后留下难复用的空洞&#34;] A --> A3[&#34;③ 分配器攒着已释放的页<br/>不主动还给操作系统&#34;] A1 --> R[&#34;分配器持有、Redis 却在用的空页&#34;] A2 --> R A3 --> R R --> C[&#34;表现:allocator_frag_bytes 上升<br/>used_memory_rss ≫ used_memory&#34;] classDef warn fill:#D82C20,stroke:#FF6B5E,color:#fff; classDef plain fill:#1E2532,stroke:#2F3949,color:#E6EDF3; class R warn; class A,A1,A2,A3,C plain;

典型场景 :你不停地 SET 又 DEL 各种大小的 key,分配器把内存切成很多块,释放后这些块无法被相邻合并利用,就形成了碎片。

3.2 怎么治:activedefrag

Redis 4.0 起提供自动碎片整理 (要求 Redis 用 jemalloc 编译,libc malloc 下不可用):

bash 复制代码
# 开启自动碎片整理(默认关闭)
config set activedefrag yes

# 相关参数
active-defrag-ignore-bytes 100mb      # 碎片达到 100MB 才启动
active-defrag-threshold-lower 10      # 碎片率 >10% 开始整理
active-defrag-threshold-upper 100     # 碎片率 >100% 全力整理
active-defrag-cycle-min 1             # 整理占 CPU 下限 1%
active-defrag-cycle-max 25            # 整理占 CPU 上限 25%

3.3 手动整理

bash 复制代码
# 4.0+ 提供命令(仅 jemalloc)
memory purge

若版本太老、或不是 jemalloc 编译,只能通过重启(或用主从切换)来回收碎片。


四、过期删除策略:惰性删除 + 定期删除

给 key 设过期时间后,Redis 怎么"删"它?这里有三种候选方案,Redis 只用了后两种:

策略 做法 优点 缺点 Redis 是否采用
定时删除 到点就删,用定时器 内存最省,及时 海量 key 时 CPU 爆炸 ❌ 不用
惰性删除 访问到才检查是否过期 CPU 友好 不访问就永远不删,内存泄漏 ✅ 用
定期删除 每隔一段时间抽一批删 折中 难以精确控制 ✅ 用

💡 为什么不用定时删除? 假设有 100 万个 key 都在同一时刻过期,定时删除要创建 100 万个定时器任务,CPU 直接被拖垮。用 CPU 换那点内存不划算,所以 Redis 选择"惰性 + 定期"组合:惰性兜底正确性,定期兜底回收。

4.1 惰性删除(Lazy Expiration)

flowchart TD A[&#34;客户端执行 GET k&#34;] --> B{&#34;k 已过期?&#34;} B -- 否 --> C[&#34;正常返回值&#34;] B -- 是 --> D[&#34;删除 k&#34;] D --> E[&#34;返回 nil&#34;] classDef key fill:#D82C20,stroke:#FF6B5E,color:#fff; classDef plain fill:#1E2532,stroke:#2F3949,color:#E6EDF3; class B key; class A,C,D,E plain;

实现位置:expireIfNeeded(),在读写命令真正执行前调用。

缺点 :如果一个 key 过期后再也没人访问,它就永远不会被删除,内存一直被占。所以必须靠定期删除来兜底。

4.2 定期删除(Active Expiration)

Redis 由 serverCron 驱动,默认每秒 10 次 (hz=10,即每 100ms 一轮),逻辑大致是:

flowchart TD S[&#34;serverCron 每 100ms 触发(hz=10)&#34;] --> A[&#34;从该库 expires 字典<br/>随机抽 20 个 key&#34;] A --> D[&#34;删掉其中已过期的<br/>未过期的原样保留&#34;] D --> B{&#34;本轮过期比例 > 25%?&#34;} B -- &#34;是&#34; --> E{&#34;本轮耗时<br/>超预算了?&#34;} E -- &#34;否&#34; --> A E -- &#34;是&#34; --> F[&#34;结束本轮&#34;] B -- &#34;否&#34; --> F F[&#34;等下一个 cron 周期&#34;] classDef loop fill:#F59E0B,stroke:#FFD98A,color:#1A1A1A; classDef plain fill:#1E2532,stroke:#2F3949,color:#E6EDF3; class B,E loop; class S,A,D,F plain;

核心参数:

conf 复制代码
hz 10                    # 每秒 cron 次数,默认 10
dynamic-hz yes           # 默认开:客户端多时自动把 hz 调高
active-expire-effort 1   # 1~10,默认 1;越大越努力,也越耗 CPU
  • 抽 20 个 key,如果过期比例超过 25% ,说明这批 key 大多是"尸体",就继续抽------循环直到比例降下来。
  • hz 越高、active-expire-effort 越大,过期 key 回收越及时,但 CPU 开销越大。active-expire-effort 会同时放大每轮采样数量 和时间预算。
  • 为避免抢走命令执行的 CPU,每轮有时间预算:默认不超过一个 cron 周期的 25%(即 100ms 周期里的 ~25ms)。
  • 官方目标是:默认 effort 下,内存里残留的过期 key 不超过约 10%。

💡 示例:你在凌晨批量写入 10 万个 60 秒过期的 key,全部在同一秒过期。

  • 惰性删除:这 10 万个 key 你不访问它就不删;
  • 定期删除:每 100ms 抽 20 个,因为过期比例远超 25%,会连续抽很多轮(直到时间预算用完),下一轮 cron 接着清,最终把这批 key 清理掉------分摊在多个周期里,不会一次性卡死主线程。

五、内存淘汰策略:maxmemory-policy 怎么选

当内存达到 maxmemory,Redis 就会按 maxmemory-policy 淘汰一些 key,腾出空间。

bash 复制代码
# 设置上限,且必须显式配置策略(默认 noeviction 会直接报错返回)
config set maxmemory 2gb
config set maxmemory-policy allkeys-lru

5.1 策略总览(8.6 起 10 种,≤ 8.4 是 8 种)

flowchart TD A[&#34;maxmemory-policy&#34;] --> B[&#34;不淘汰(noeviction)&#34;] A --> C[&#34;在『设置了过期时间』的 key 里淘汰<br/>volatile-*&#34;] A --> D[&#34;在『所有 key』里淘汰<br/>allkeys-*&#34;] C --> C1[&#34;volatile-lru 最近最少使用&#34;] C --> C2[&#34;volatile-lfu 最不经常使用&#34;] C --> C3[&#34;volatile-lrm 最久未修改 8.6+&#34;] C --> C4[&#34;volatile-ttl 剩余寿命最短&#34;] C --> C5[&#34;volatile-random 随机&#34;] D --> D1[&#34;allkeys-lru 最常用&#34;] D --> D2[&#34;allkeys-lfu 热点明显&#34;] D --> D3[&#34;allkeys-lrm 只留最近更新过的 8.6+&#34;] D --> D4[&#34;allkeys-random 访问均匀&#34;] classDef no fill:#D82C20,stroke:#FF6B5E,color:#fff; classDef vol fill:#F59E0B,stroke:#FFD98A,color:#1A1A1A; classDef all fill:#3B82F6,stroke:#9FD0FF,color:#fff; class B no; class C,C1,C2,C3,C4,C5 vol; class D,D1,D2,D3,D4 all;
策略 淘汰范围 淘汰规则 适用场景 版本
noeviction --- 不淘汰,写入直接报错 数据库/不允许丢数据 默认值
volatile-lru 有 TTL 的 key 最近最少使用 冷热分明,且都给 TTL ---
volatile-lfu 有 TTL 的 key 最不经常使用 有热点访问且给 TTL 4.0+
volatile-ttl 有 TTL 的 key 剩余存活时间最短 用 TTL 表达"淘汰优先级" ---
volatile-random 有 TTL 的 key 随机 无明显冷热 ---
volatile-lrm 有 TTL 的 key 最久未被修改 读多写少,想淘汰陈旧数据 8.6+
allkeys-lru 所有 key 最近最少使用 最常用,纯缓存 ---
allkeys-lfu 所有 key 最不经常使用 热点明显的纯缓存 4.0+
allkeys-lrm 所有 key 最久未被修改 想留下"最近更新过"的 8.6+
allkeys-random 所有 key 随机 访问概率均匀 ---

⚠️ 最大的坑:volatile-* 系列如果 key 都没设 TTL,会导致"无 key 可淘汰",行为退化成 noeviction! 比如你把策略设成 volatile-lru,但业务写入时忘了设过期时间 ,那内存满了之后 Redis 找不到任何"带 TTL 的 key"可淘汰,就会直接对写命令返回错误:

vbnet 复制代码
(error) OOM command not allowed when used memory > 'maxmemory'.

结论 :用 volatile-* 就必须保证 key 都设了过期时间;否则用 allkeys-*。

顺带一提:官方更推荐把缓存和持久数据放两个实例 ,而不是靠 volatile-* 在一个实例里混着------后者既多花 expires 字典的内存,又容易踩上面这个坑。

5.2 怎么选?

一句话经验:

  • 纯缓存、能丢 → allkeys-lru(最稳)或 allkeys-lfu(热点明显)。
  • 缓存 + 有必须保留的数据 → volatile-lru,但务必给可淘汰的 key 都设 TTL(或者干脆拆两个实例)。
  • 绝不能丢 → noeviction,但要配好内存告警,别等它写不进去才发现。

六、LRU vs LFU:Redis 是怎么"近似"的

这是面试高频考点:Redis 的 LRU 不是真正的 LRU,LFU 也不是真正的 LFU,都是为了省内存做的"近似实现"。

6.1 为什么不用真正的 LRU?

真正的 LRU 需要一个双向链表 + 哈希表 ,每个 key 光是链表指针就要 16 字节,而 Redis 全部家当只有 24 位可分配。内存开销和 CPU 开销(每次访问都要移动节点)都太大。

Redis 的做法:采样 + 时钟。

6.2 近似 LRU:24 位时钟

还记得 redisObject 里的 lru:24 吗?它存的是**「最近一次访问时,全局 lruclock 的快照」------注意这不是 Unix 时间戳,而是 (mstime() / 1000) & 0xFFFFFF,即秒级精度、24 位、约 194 天回绕**的相对时钟。

flowchart TD A[&#34;需要淘汰内存时&#34;] --> B[&#34;随机采样 N 个 key<br/>(maxmemory-samples,默认 5)&#34;] B --> C[&#34;比较各 key 的 lru 字段<br/>找到最久未访问的&#34;] C --> D[&#34;淘汰它&#34;] D --> E[&#34;为减少误差,Redis 3.0+ 维护<br/>淘汰池(eviction pool)&#34;] classDef pool fill:#3B82F6,stroke:#9FD0FF,color:#fff; classDef plain fill:#1E2532,stroke:#2F3949,color:#E6EDF3; class E pool; class A,B,C,D plain;

💡 关键点 :淘汰比较的是「快照值的差值」,所以这个时钟回绕是安全的------Redis 用无符号减法算"过了多久",194 天回绕不会让比较结果错乱。

关键参数:

conf 复制代码
maxmemory-samples 5      # 采样数量,越大越精确但越耗 CPU

💡 采样数的影响:

  • samples=5:性能好,但淘汰的 key 可能不是最优的(近似);
  • samples=10:更接近真实 LRU,但每次淘汰要比较更多 key。

生产上一般 5 就够,对精度要求极高可调到 10。

6.3 LFU:访问频率打败"新鲜度"

LRU 有个致命缺陷:一个 key 只要偶尔被访问一次,就会"焕然一新",导致真正的热点被误淘汰。

比如:某 key 昨天被访问 10 万次,今天没人访问;另一个 key 刚刚被访问 1 次。LRU 会认为第二个更"新",淘汰第一个------这不合理。

LFU(Least Frequently Used,Redis 4.0+)按访问频率淘汰。

LFU 复用同一个 24 位字段,拆成两部分(antirez 的原始设计):

diff 复制代码
     高 16 位(ldt)              低 8 位(counter)
+---------------------------+---------------+
| 最近一次衰减时间(分钟级)  |  对数计数器 0~255 |
+---------------------------+---------------+
flowchart LR A[&#34;24 位 LFU 字段&#34;] --> B[&#34;高 16 位<br/>最近衰减时间(分钟)&#34;] A --> C[&#34;低 8 位<br/>访问计数器 0~255&#34;] C --> D[&#34;概率递增<br/>计数越大越难增加&#34;] C --> E[&#34;时间衰减<br/>久不访问自动降&#34;] classDef hi fill:#3B82F6,stroke:#9FD0FF,color:#fff; classDef lo fill:#D82C20,stroke:#FF6B5E,color:#fff; classDef act fill:#1E2532,stroke:#2F3949,color:#E6EDF3; class B hi; class C lo; class D,E act;

两个精妙设计:

  1. 概率递增(对数递增) :计数器不是每次访问都 +1,而是访问越频繁,增长越难 ------递增概率约为 1 / (lfu-log-factor × counter)。

    • 计数器为 0 时,+1 的概率是 1;
    • 计数越大,+1 概率越低。
    • 目的:避免"新 key 因频繁访问瞬间涨到 255,秒杀老热点"。
  2. 时间衰减 :每过 lfu-decay-time 分钟没被访问,计数器就减 1(下限 5,默认 1 分钟减 1)。

    • 目的:让"曾经的王者"重新参与竞争,避免老热点永远霸榜。

相关参数:

conf 复制代码
lfu-log-factor 10        # 计数器递增难度因子:值越大,计数器越难增长(区分度越低)
lfu-decay-time 1         # 衰减周期:计数器每多长时间(分钟)衰减 1 次;值越大衰减越慢

记忆:lfu-log-factor 控制"涨得多难",lfu-decay-time 控制"掉得多快"。
💡 LRU vs LFU 一句话:

  • LRU 看"多久没用"(时间维度)------适合访问模式随时间变化的场景;
  • LFU 看"用了多少次"(频率维度)------适合有明显稳定热点的场景。

典型例子:新闻 App 的"今日热榜"用 LFU 更好(点击多的稳定是热点);而用户会话 token 用 LRU 更好(谁最近登录谁活跃)。


七、淘汰的完整触发流程

把前面串起来,一次内存淘汰的完整链路是:

flowchart TD A[&#34;客户端执行命令&#34;] --> B{&#34;这条命令会增加内存吗?&#34;} B -- &#34;不会(GET/DEL 等)&#34; --> Z[&#34;直接执行,不检查&#34;] B -- &#34;会&#34; --> C[&#34;检查 used_memory 是否 > maxmemory&#34;] C -- 否 --> Z C -- 是 --> D{&#34;maxmemory-policy&#34;} D -- &#34;noeviction&#34; --> E[&#34;写命令返回 OOM 错误&#34;] D -- &#34;volatile-*&#34; --> F[&#34;从『过期字典』中采样淘汰&#34;] D -- &#34;allkeys-*&#34; --> G[&#34;从『所有 key』中采样淘汰&#34;] F --> H{&#34;找到可淘汰的 key?&#34;} G --> H H -- &#34;否(如 volatile-* 但无 TTL)&#34; --> E H -- &#34;是&#34; --> I[&#34;采样 N 个 → 淘汰最优者&#34;] I --> J[&#34;释放内存&#34;] J --> C Z --> K[&#34;返回结果&#34;] classDef err fill:#D82C20,stroke:#FF6B5E,color:#fff; classDef ok fill:#3FB950,stroke:#7EE787,color:#fff; classDef plain fill:#1E2532,stroke:#2F3949,color:#E6EDF3; class E err; class Z,K ok; class A,B,C,D,F,G,H,I,J plain;

注意:

  • 淘汰发生在命令执行前 ,而且是循环 进行的------每淘汰一个 key 就重新检查内存,直到降到 maxmemory 以下或没有可淘汰对象。
  • 只读命令(以及 DEL 等不增加内存的命令)不会 触发淘汰检查;所以 noeviction 下是"写不进去、读照常"。
  • 参与比较的是 used_memory,但 repl-backlog、AOF 缓冲、客户端缓冲这类内存不计入 (Redis 7.0+ 客户端缓冲由 maxmemory-clients 单独管)。

八、版本差异与部署形态差异(内存角度)

前面讲的是「通用原理」,但生产环境还要看版本和部署形态,否则容易踩坑。

8.1 与内存/淘汰相关的版本差异

特性 / 参数 版本 说明
allkeys-lrm / volatile-lrm 8.6+ 新增 LRM 淘汰,策略数从 8 种变 10 种
maxmemory-clients 7.0+ 所有客户端缓冲区总量 上限(可设 0 表示不限,默认 0,或百分比如 10%)。7.0 之前客户端缓冲计入 maxmemory 淘汰判断,升级后行为会变
client-query-buffer-limit 7.0+ 单连接输入缓冲上限(默认 1gb,旧版本是硬编码)
maxmemory-eviction-tenacity 7.0+ 淘汰努力程度 0~100,越高越不易放弃淘汰,但越耗 CPU
replica-ignore-maxmemory 7.4+ 默认 yes:从库忽略 maxmemory,不淘汰;设为 no 才会触发从库淘汰(危险,见 9.2 坑 4)
SET ... KEEPTTL 6.0+ 覆盖写保留 TTL
GETEX / GETDEL 6.2+ 读取时设置/清除 TTL,或读取并删除
lazyfree-lazy-user-flush 6.2+ FLUSHDB / FLUSHALL 异步化
io-threads 6.0+ 网络 I/O 多线程(命令执行仍是单线程)
volatile-lfu / allkeys-lfu 4.0+ 3.x 只有 LRU,没有 LFU
UNLINK / MEMORY PURGE / MEMORY USAGE / OBJECT FREQ/IDLETIME / activedefrag 4.0+ 排查工具,老版本不可用

8.2 Cluster 与代理模式的差异(很关键)

  • Cluster 模式下,每个分片节点有自己独立的 maxmemory 和 maxmemory-policy 。设「整个集群 100GB」是无效的------要按分片数均摊去设每一台。
  • 热点不均会导致部分分片先 OOM :即使总量还有余量,落到热点分片上的写入依然会报 OOM command not allowed。排查时不能只看集群总用量。
  • 代理模式(部分云厂商 / Codis / twemproxy) :内存统计口径可能不同,INFO memory 看到的可能是代理层视角;maxmemory 与淘汰策略也可能由平台托管,改配置的入口在控制台而不在 redis.conf。
  • 云托管 Redis 的常见限制 :不允许 CONFIG SET 某些项、不提供 MEMORY PURGE、activedefrag 由平台控制、maxmemory 已预留 COW 空间(所以控制台里看到的可用上限往往已是打过折的)。迁移自建 → 云时,先确认这几个参数谁说了算。


九、面试追问 Q1--Q15

Q1:Redis 过期的 key 是怎么删除的?

惰性删除 + 定期删除。惰性删除在每次访问前用 expireIfNeeded() 判断;定期删除由 serverCron 每 100ms(hz=10)触发,从 expires 字典随机抽 20 个 key,删掉已过期的,若过期比例 > 25% 就继续抽,直到比例下降或时间预算(约 25ms)用完。不用定时删除是因为海量 key 同时过期会让 CPU 崩溃。

Q2:为什么 Redis 的 LRU 是"近似 LRU"?

真正 LRU 要维护双向链表,内存/CPU 开销大(光链表指针每 key 就要 16 字节)。Redis 在 redisObject.lru 存 24 位访问时钟(相对全局 lruclock 的秒级快照,194 天回绕),淘汰时随机采样 maxmemory-samples(默认 5)个 key ,淘汰其中最久未访问的,并用淘汰池降低误差。

Q3:LFU 和 LRU 的区别?

LRU 按"最近一次访问时间"淘汰,LFU 按"访问频率"淘汰。LFU 用 24 位中的高 16 位存衰减时间、低 8 位存计数器,计数器概率递增 + 时间衰减。LFU 避免了"偶尔被访问一次的冷 key 把真热点挤掉"的问题。

Q4:volatile-lru 和 allkeys-lru 的区别?

前者只淘汰设置了过期时间 的 key,后者淘汰所有 key。若用 volatile-* 但 key 都没设 TTL,内存满时会退化为 noeviction,直接 OOM 报错。

Q5:used_memory_rss 比 used_memory 小说明什么?

说明发生了 swap,部分内存被换到磁盘。逻辑内存没变但物理页不在内存里,访问会触发磁盘 IO,延迟从微秒级升到毫秒级,是严重问题,应立即排查并关闭 swap。

Q6:内存碎片率多少算正常?怎么处理?

mem_fragmentation_ratio = rss / used_memory。1~1.5 正常;>1.5 需关注;<1 说明 swap。这个口径混入了 COW 与分配器元数据,看真碎片要看 allocator_frag_bytes。处理方式:开启 activedefrag(需 jemalloc)、MEMORY PURGE(手动),或重启实例。

Q7:maxmemory 满了但策略是 noeviction,会发生什么?

所有会申请内存的写命令 返回错误 OOM command not allowed when used memory > 'maxmemory',读命令以及 DEL 等不增内存的命令正常。这是"拒绝写入"的保护机制。

Q8:主从架构下过期 key 怎么处理?

从库不主动 删过期 key,由主库删除后同步 DEL 给从库。从库读过期 key 时,3.2 之前返回过期数据、3.2 之后返回 nil 但仍保留直到主库同步删除,因此主从延迟期间可能读到"逻辑已过期但物理还在"的数据。

Q9:DEL 和 UNLINK 有什么区别?

DEL 同步删除并释放内存,删大 key 会阻塞主线程;UNLINK 只把 key 从空间摘除,实际内存释放交给后台线程异步完成,适合删除大 key。

Q10:SET k v 会不会影响已有的 TTL?

会。普通 SET(不带 KEEPTTL)会清空 key 已有的过期时间,让其变回永不过期(TTL 返回 -1)。覆盖写时要么显式带 EX,要么 Redis 6.0+ 用 SET ... KEEPTTL 保留 TTL。这是「TTL 静默丢失 → 内存只涨不降」的头号事故源。

Q11:lazyfree 和 UNLINK 是什么关系?

UNLINK 是手动指定这一次删除走异步 ;lazyfree-lazy-* 系列开关则是让 Redis 在对应场景自动 走后台线程异步释放,比如 lazyfree-lazy-eviction、lazyfree-lazy-expire、lazyfree-lazy-user-del。两者本质相同:都只是从主线程摘除 key,真正释放由 bio 后台线程完成,解决的是「阻塞」而非「内存占用」。

Q12:fork 时 COW 会多占多少内存?

取决于 fork 之后被写入的 key 数量,而不是总内存大小。子进程只读不复制;父进程每改一个 key,对应内存页就被复制一份。最坏情况下全量 key 都被写,COW 复制量接近 100% 内存,峰值可达平时的 2 倍。所以要控制单实例大小、错峰持久化、关闭 THP。

Q13:maxmemory-clients 解决什么问题?

Redis 7.0+ 提供的所有客户端缓冲区总量上限 (字节数或百分比,如 5%;默认 0 = 不限)。7.0 之前客户端缓冲计入 maxmemory 淘汰判断,可能被无限撑大并连带淘汰 key;7.0 之后它被摘出 maxmemory,超限后 Redis 丢的是客户端连接而不是 key。

Q14:Cluster 模式下 maxmemory 怎么设?

每个分片节点独立生效,要按分片数均摊去设每一台------设「整个集群 100GB」是无效的。还要警惕热点不均导致部分分片先 OOM:集群总用量有余量,不代表热点分片不报错。

Q15:从库会自己淘汰 key 吗?

默认不会。replica-ignore-maxmemory yes(默认,7.4 起可配置)让从库忽略 maxmemory,淘汰只由主库发起并以 DEL 同步过去------这保证了主从一致。代价是从库内存可能超过 maxmemory,监控要单独设阈值。如果手动把它关掉,从库就会独立淘汰,主从数据立刻分叉。


如果这篇对你有帮助,欢迎点赞收藏,也欢迎在评论区补充你踩过的内存坑 👇


相关推荐
弈栈录2 小时前
从 JVM 内存模型到 GC 调优:Java 服务性能优化实战
java
shuoshuohaohao2 小时前
Spring-boot-3 -注解 yaml配置 -日志
java·数据库·mysql
青春逝如流水2 小时前
Java 线程池的创建方式、注意要点与任务类型判断
java·开发语言·线程池·多线程·ai文章
用户3610588626122 小时前
深入理解Scala模式匹配:结构化解构
java
开开心心_Every2 小时前
内存清理工具定时自动清理开机自启动
java·开发语言·spring·计算机外设·ocr·intellij-idea·excel
李福春2 小时前
斗破Google ADK Java苍穹-第5境斗灵-凝物通脉-Context与State
java·开发语言·人工智能·java-ee·google adk·深圳同盟·腾讯云架构师技术同盟
十年Java程序媛2 小时前
Java 接口和抽象类对比|抛弃老旧八股,正确选型
java·开发语言·spring boot·后端
范中勤2 小时前
OSS 多文件打包回流上传:流式管道无落地 VS 临时磁盘落地,两种工业方案深度对比
java·高并发·zip·oss·架构选型
无毁的湖光-Al3 小时前
解Bug之路-隔离故障点后业务不恢复?-热点Key处理与亚稳态故障
java·数据库·mysql