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

58 阅读15分钟

刚学完 Redis 的内存管理和过期淘汰策略,知识点又多又碎:used_memory 和 used_memory_rss 到底差在哪?为什么 Redis 不用"定时删除"?volatile-lru 和 allkeys-lru 只差一个前缀,效果却天差地别;maxmemory-policy 到底该选哪个? 这篇文章我把内存从哪来 → 怎么管 → 过期怎么删 → 满了怎么淘汰串成一条线,配图 + 示例 + 面试追问,一次讲清楚。建议收藏,配合 redis-cli 边看边敲。

想要搞清楚 Redis 内存,先来看一下redis的内存是如何管理的:

flowchart TD
    A["Redis 内存管理"] --> B["内存都花在哪?"]
    A --> C["怎么查看内存?"]
    A --> D["内存碎片怎么处理?"]
    A --> E["过期键怎么删除?"]
    A --> F["内存满了怎么淘汰?"]

    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;

一、内存到底花在哪了?

先记住一条底线:

used_memory = Redis 通过自己的分配器(zmalloc)申请到的字节数; used_memory_rss = 操作系统看到这个进程真正占着的物理页。 两者不是包含关系:碎片只在 RSS 侧,swap 只让 RSS 变小。

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;

⚠️ 最容易被画错的一点:碎片不在 used_memory 里。碎片是「Redis 已还给分配器、但分配器攥着不还给 OS」的页,只体现在 RSS 侧。

1.1 对象开销:小 key 的真实成本

每个 value 都包一层 redisObject(64 位机器上 16 字节),key 还要额外占全局哈希表的 dictEntry(约 24 字节)。

💡 SET a 1 看似只存 1 字节,实际至少是 dictEntry(24B) + key 的 SDS + redisObject(16B),加上分配器对齐、哈希表槽位摊销——一条小记录往往在 50~100 字节量级。所以存海量小 key 时,实际内存是「数据体积」的几倍。

1.2 缓冲区(OOM 高发区)

客户端输入/输出缓冲、repl-backlog、AOF 缓冲都吃内存,且空闲连接的输出缓冲同样计入 used_memory。

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

⚠️ 一个真实事故点:normal 默认 0 0 0 = 完全不限。慢客户端 + 一次大结果集(误用 KEYS *、拉十万条 Hash 字段),输出缓冲就能吃掉几 GB。


二、怎么查看内存:INFO memory

redis-cli info memory
字段含义怎么看
used_memory分配器统计的逻辑内存Redis 自己认为用了多少
used_memory_rssOS 看这个进程占的物理内存top / ps 看到的那个值
mem_fragmentation_ratio碎片率 = rss / used_memory粗口径,判断整体健康度
allocator_frag_bytes分配器纯碎片字节排查碎片的精确口径(需 jemalloc)
used_memory_overhead对象、字典、缓冲区等开销总和开销过高说明小 key 太多
used_memory_datasetused_memory − overhead约等于数据集本身
used_memory_peak_perc当前 / 历史峰值 的百分比判断是否长期高位运行
maxmemory / maxmemory_policy容量上限 / 淘汰策略见第五节
used_memory_scriptsLua 脚本占用脚本缓存泄漏

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

2.1 碎片率怎么读?

比值含义处理
1 ~ 1.5正常无需处理
> 1.5碎片较多看 allocator_frag_bytes 确认,再考虑整理
< 1RSS 比逻辑内存还小发生了 Swap,性能灾难
远大于 1(> 3)严重碎片 / fork 期间排查并整理

💡 为什么 RSS 可能小于 used_memory? 操作系统把部分内存换出到磁盘了。逻辑上 Redis 认为自己有这些内存,物理上被挪走——访问变成磁盘 IO,延迟从微秒飙到毫秒,线上必须关闭 swap。

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

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

命令 / 工具用途
MEMORY USAGE key单 key 实际占用字节数,定位大 key 的权威手段(比 STRLEN 准)
MEMORY DOCTOR内存问题诊断建议,内存涨/降不下来时先跑
MEMORY PURGE手动回收碎片(4.0+,仅 jemalloc,阻塞主线程,低峰期执行)
OBJECT FREQ keyLFU 访问计数器(需 *-lfu 策略)
redis-cli --memkeys按内存占用排序找大 key(6.0+)
redis-cli --bigkeys找元素最多的 key,会占 CPU
SLOWLOG GET看大 key 删除/淘汰导致的慢命令
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 <key>                # 5. 抽样确认 TTL 没被清掉(返回 -1 就是坑)
redis-cli slowlog get 10           # 6. 有没有大 key 导致的慢命令

三、内存碎片:怎么治

碎片来自频繁增删改、大小跨度大、分配器攒着已释放的页。表现就是 allocator_frag_bytes 上升、rss ≫ used_memory。

自动整理(Redis 4.0+,要求 jemalloc 编译,libc malloc 下不可用):

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%

版本太老或非 jemalloc,只能靠重启(或主从切换)回收。


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

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

💡 为什么不用定时删除? 100 万个 key 同时过期,定时删除要创建 100 万个定时器,CPU 直接被拖垮。用 CPU 换那点内存不划算。

  • 惰性删除:实现是 expireIfNeeded(),在读写命令执行前调用。过期后再没人访问就永远不删 → 必须靠定期删除兜底。
  • 定期删除:serverCron 每 100ms(hz=10)触发,从 expires 字典随机抽 20 个 key,删掉已过期的;若过期比例 > 25% 就继续抽,直到比例降下来或时间预算(约 25ms)用完。
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;
hz 10                    # 每秒 cron 次数,默认 10
dynamic-hz yes           # 默认开:客户端多时自动调高 hz
active-expire-effort 1   # 1~10,越大越努力,也越耗 CPU

💡 示例:凌晨批量写入 10 万个 60 秒过期的 key。定期删除会每 100ms 抽 20 个,因过期比例远超 25% 而连续抽很多轮(直到预算用完),下一轮 cron 接着清——分摊在多个周期,不会一次性卡死主线程。官方目标是默认 effort 下残留过期 key 不超过约 10%。

4.1 TTL 是怎么被悄悄弄丢的

expires 是独立字典,存绝对时间戳(Unix 毫秒),每个带 TTL 的 key 多一份 dictEntry 开销。

最高频的坑是 SET 覆盖写清空 TTL:

127.0.0.1:6379> SET k v EX 3600
127.0.0.1:6379> TTL k
(integer) 3596
127.0.0.1:6379> SET k v2        # ⚠️ 不带 KEEPTTL,TTL 被清空
127.0.0.1:6379> TTL k
(integer) -1                    # 变成永不过期了!

解法:覆盖写用 SET k v2 KEEPTTL(6.0+)或每次显式带 EX;INCR/APPEND/HSET/LPUSH 这类改值命令则保留 TTL。

💡 排查「内存为什么不降」第一步:抽样 TTL。大量返回 -1 说明 TTL 被清掉了,不是过期策略失效——这个区分能省掉大量无用排查。


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

内存达到 maxmemory 时,Redis 按 maxmemory-policy 淘汰 key 腾空间。

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—不淘汰,写入直接报错数据库/不允许丢数据(默认值)
allkeys-lru所有 key最近最少使用最常用,纯缓存
allkeys-lfu所有 key最不经常使用热点明显的纯缓存
allkeys-lrm所有 key最久未被修改读多写少(8.6+)
allkeys-random所有 key随机访问概率均匀
volatile-lru有 TTL 的 key最近最少使用冷热分明,且都给 TTL
volatile-lfu有 TTL 的 key最不经常使用有热点访问且给 TTL
volatile-ttl有 TTL 的 key剩余存活时间最短用 TTL 表达淘汰优先级
volatile-lrm有 TTL 的 key最久未被修改淘汰陈旧数据(8.6+)
volatile-random有 TTL 的 key随机无明显冷热

⚠️ 最大的坑:volatile-* 如果 key 都没设 TTL,会无 key 可淘汰,行为退化成 noeviction,写命令直接报错:

(error) OOM command not allowed when used memory > 'maxmemory'.

结论:用 volatile-* 就必须保证 key 都设了过期时间;否则用 allkeys-*。官方更推荐把缓存和持久数据放两个实例,而不是在一个实例里混着。

5.1 怎么选?

flowchart TD
    S1{&#34;能接受丢数据吗?&#34;} -- 不能 --> R1[&#34;noeviction<br/>(配合持久化/告警)&#34;]
    S1 -- 能 --> S2{&#34;所有 key 都能丢吗?&#34;}
    S2 -- 能 / 纯缓存 --> S3{&#34;有明显热点吗?&#34;}
    S3 -- 有 --> R2[&#34;allkeys-lfu<br/>(或 allkeys-lru)&#34;]
    S3 -- 没有 --> R3[&#34;allkeys-random&#34;]
    S2 -- 只有部分能丢 --> S4{&#34;这些 key 都有 TTL 吗?&#34;}
    S4 -- 有 --> R4[&#34;volatile-lru / volatile-lfu&#34;]
    S4 -- 没有 --> R5[&#34;先补上 TTL,否则退化成 noeviction&#34;]

    classDef dec fill:#1E2532,stroke:#58A6FF,color:#E6EDF3;
    classDef res fill:#D82C20,stroke:#FF6B5E,color:#fff;
    class S1,S2,S3,S4 dec;
    class R1,R2,R3,R4,R5 res;

一句话经验:

  • 纯缓存、能丢 → allkeys-lru(最稳)或 allkeys-lfu(热点明显)。
  • 缓存 + 有必须保留的数据 → volatile-lru,但务必全员带 TTL。
  • 绝不能丢 → noeviction,但要配好内存告警。

5.2 lazyfree:删除不阻塞主线程

DEL 删大 key 会同步释放内存、阻塞主线程。UNLINK 只把 key 从空间摘除,真正释放交给后台 bio 线程;lazyfree-* 则是让 Redis 在对应场景自动异步释放。

# 大 key 较多的实例推荐(均默认 no)
lazyfree-lazy-eviction yes      # 内存淘汰时
lazyfree-lazy-expire yes        # 过期删除时
lazyfree-lazy-server-del yes    # RENAME / SET 覆盖旧值时
lazyfree-lazy-user-del yes      # 用户 DEL(等价全部 DEL 变 UNLINK)
lazyfree-lazy-user-flush yes    # FLUSHALL / FLUSHDB(6.2+)

⚠️ 代价:异步释放意味着内存不会立即归还,used_memory 下降是滞后的。它解决的是**「阻塞」不是「占用」**。


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

Redis 的 LRU 不是真正的 LRU,LFU 也不是真正的 LFU,都是为了省内存做的近似实现。

真正的 LRU 需要双向链表 + 哈希表,每个 key 光链表指针就要 16 字节,而 Redis 全部家当只有 24 位可分配。

6.1 近似 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 天回绕不会让比较错乱。

samples=5 性能好但近似;要求精度可调到 10。生产上 5 就够。

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

LRU 的致命缺陷:一个 key 偶尔被访问一次就"焕然一新",导致真正的热点被误淘汰(某 key 昨天访问 10 万次,另一个刚访问 1 次,LRU 会淘汰前者)。

LFU(4.0+)复用同一个 24 位字段:

     高 16 位(ldt)              低 8 位(counter)
+---------------------------+---------------+
| 最近一次衰减时间(分钟级)  |  对数计数器 0~255 |
+---------------------------+---------------+

两个精妙设计:

  1. 概率递增:计数器不是每次 +1,递增概率约为 1 / (lfu-log-factor × counter)——访问越频繁增长越难,避免「新 key 瞬间涨到 255 秒杀老热点」。
  2. 时间衰减:每过 lfu-decay-time 分钟没访问,计数器减 1(下限 5)。让「曾经的王者」重新参与竞争。
lfu-log-factor 10        # 控制"涨得多难",值越大越难增长
lfu-decay-time 1         # 控制"掉得多快",单位分钟

💡 一句话:LRU 看"多久没用"(时间维度),适合访问模式随时间变化;LFU 看"用了多少次"(频率维度),适合有明显稳定热点。新闻热榜用 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;

注意:

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

八、生产配置与实践建议

8.1 推荐配置片段

maxmemory 4gb                          # 按实例可用内存的 60%~75% 设置
maxmemory-policy allkeys-lru           # 纯缓存最稳的选择
maxmemory-samples 5                    # LRU/LFU 采样数,5~10
maxmemory-clients 5%                   # 7.0+:客户端缓冲总量兜底,别留 0
hz 10                                  # 定期删除频率(默认值)
active-expire-effort 1                 # 短 TTL 密集可调到 5~10
replica-ignore-maxmemory yes           # 从库保持不淘汰(默认值)
activedefrag yes                       # 需 jemalloc

8.2 为什么 maxmemory 只能设 60%~75%

快照类操作通过 fork() 创建子进程,是 COW(Copy-On-Write):父子进程共享物理页不复制数据,但 fork 本身要复制页表。

  • COW 额外吃多少内存,取决于 fork 之后被写入的 key 数量:子进程只读不复制,父进程每改一个 key,该页就被复制一份。
  • 极端情况 fork 后全量 key 都被写,COW 复制量接近 100% 内存,峰值可达平时 2 倍。
物理内存 100%
├─ Redis 数据上限(maxmemory)     60% ~ 75%
├─ COW 增量(最坏接近 100% maxmemory)
├─ 客户端输出缓冲 / 复制缓冲区
└─ 操作系统自身

规避手段(按性价比排序):① 控制单实例大小(≤ 10GB),超大实例拆 Cluster;② 错峰持久化;③ 关掉 THP(会把 COW 复制单位从 4KB 放大到 2MB);④ 监控 latest_fork_usec。

redis-cli info stats         # 关注 latest_fork_usec(最近一次 fork 耗时,微秒)
redis-cli info persistence   # rdb_bgsave_in_progress / aof_rewrite_in_progress 为 1 表示子进程在跑

8.3 六个必踩的坑

坑后果解法
volatile-* 但没设 TTL退化成 noeviction,写入全挂用 allkeys-*,或代码层强制 SET k v EX 3600
maxmemory 设太大swap、OOM Killer、COW 翻倍留 60%~75% 余量,关 swap
大 key 同步释放删除/过期阻塞主线程拆大 key + UNLINK + lazyfree-* + --memkeys 排查
从库被允许淘汰主从数据分叉保持 replica-ignore-maxmemory yes;从库内存超 maxmemory 不是 bug,单独设告警阈值
activedefrag 用力过猛cycle-max 过高 → P99 抖动控制在 25% 以内,低峰期再加大
SET 覆盖写清 TTLkey 永不过期,内存只涨不降统一 SET k v EX 或 KEEPTTL

8.4 Cluster 与云托管的差异(很关键)

  • Cluster 下每个分片有独立的 maxmemory 和策略。设「整个集群 100GB」无效,要按分片数均摊。
  • 热点不均会导致部分分片先 OOM:总量还有余量,热点分片上的写入照样报错。排查不能只看集群总用量。
  • 云托管 Redis 常见限制:不允许 CONFIG SET 某些项、不提供 MEMORY PURGE、activedefrag 由平台控制、maxmemory 已预留 COW 空间。迁移自建 → 云时先确认这几个参数谁说了算。

九、面试追问精选

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

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

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

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

Q3:LFU 和 LRU 的区别?

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

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

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

Q5:used_memory_rss 比 used_memory 小说明什么?

发生了 swap。逻辑内存没变但物理页被换到磁盘,访问变磁盘 IO,延迟从微秒升到毫秒,应立即关闭 swap。

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

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,读命令和 DEL 等正常。

Q8:DEL 和 UNLINK 有什么区别?

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

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

会。不带 KEEPTTL 的 SET 会清空 TTL(TTL 返回 -1),变永不过期。要么显式带 EX,要么用 6.0+ 的 KEEPTTL。这是「内存只涨不降」的头号事故源。

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

取决于 fork 之后被写入的 key 数量,而非总内存。子进程只读;父进程每改一个 key 复制一页。最坏接近 100% 内存,峰值达 2 倍。所以要控制单实例大小、错峰持久化、关 THP。


十、一句话记住整篇

flowchart TD
    ROOT[&#34;Redis 内存管理&#34;] --> M1[&#34;① 内存构成<br/>used_memory:数据 + 开销 + 缓冲<br/>rss 再加:碎片 + 进程开销&#34;]
    ROOT --> M2[&#34;② 监控与治理<br/>INFO memory / allocator_frag* / activedefrag&#34;]
    ROOT --> M3[&#34;③ 过期删除<br/>惰性 + 定期(无定时)&#34;]
    ROOT --> M4[&#34;④ 内存淘汰<br/>maxmemory-policy:noeviction / volatile-* / allkeys-*&#34;]
    ROOT --> M5[&#34;⑤ 近似算法<br/>LRU 24位时钟 + 采样;LFU 频率 + 衰减&#34;]

    M3 --> K1[&#34;关键:惰性兜底正确性,定期兜底回收&#34;]
    M4 --> K2[&#34;关键:volatile-* 必须配 TTL&#34;]
    M5 --> K3[&#34;关键:采样近似,samples 默认 5&#34;]

    classDef root fill:#D82C20,stroke:#FF6B5E,color:#fff;
    classDef node fill:#1E2532,stroke:#58A6FF,color:#E6EDF3;
    classDef key fill:#F59E0B,stroke:#FFD98A,color:#1A1A1A;
    class ROOT root;
    class M1,M2,M3,M4,M5 node;
    class K1,K2,K3 key;

内存三分账,碎片只在 RSS 侧;过期靠惰性 + 定期,淘汰认准 allkeys-lru;LRU 看时钟、LFU 看频率,采样近似省开销。

三条通用建议:

  1. 给缓存 key 都设 TTL,覆盖写用 KEEPTTL 或显式带 EX,别让 TTL 被悄悄清掉;
  2. maxmemory 留足余量(物理内存 60%~75%),关掉 swap,控制单实例大小;
  3. 纯缓存用 allkeys-lru / allkeys-lfu,绝不丢数据用 noeviction,用 volatile-* 就必须全员带 TTL。

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