刚学完 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["数据本体 + 对象开销<br/>+ 客户端/复制/AOF 缓冲"]
C --> C1["INFO memory<br/>used_memory / rss / frag"]
D --> D1["activedefrag 自动碎片整理"]
E --> E1["惰性删除 + 定期删除"]
F --> F1["maxmemory-policy 全景<br/>LRU / LFU / LRM / 随机 / TTL"]
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["used_memory:Redis 自己记账的逻辑内存"]
direction TB
S1["① 数据本体<br/>(key + value)"]
S2["② 结构开销<br/>redisObject / dictEntry<br/>SDS 头 / expires 字典"]
S3["③ 缓冲区<br/>客户端输入输出缓冲<br/>repl-backlog / AOF buffer"]
end
S1 --> R0
S2 --> R0
S3 --> R0
R0["④ 碎片<br/>分配器已释放但没还给 OS 的页"]
R2["⑤ 分配器元数据、代码段与栈、<br/>fork COW 副本页等(不计入 used_memory)"]
R0 --> RSS["used_memory_rss<br/>操作系统视角的物理内存"]
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_rss | OS 看这个进程占的物理内存 | top / ps 看到的那个值 |
mem_fragmentation_ratio | 碎片率 = rss / used_memory | 粗口径,判断整体健康度 |
allocator_frag_bytes | 分配器纯碎片字节 | 排查碎片的精确口径(需 jemalloc) |
used_memory_overhead | 对象、字典、缓冲区等开销总和 | 开销过高说明小 key 太多 |
used_memory_dataset | used_memory − overhead | 约等于数据集本身 |
used_memory_peak_perc | 当前 / 历史峰值 的百分比 | 判断是否长期高位运行 |
maxmemory / maxmemory_policy | 容量上限 / 淘汰策略 | 见第五节 |
used_memory_scripts | Lua 脚本占用 | 脚本缓存泄漏 |
💡 判读思路:
used_memory_overhead / used_memory长期偏高(> 30%),基本可断定存了海量小 key,优化方向是「合并 key、加大 value 粒度」,而不是调淘汰策略。
2.1 碎片率怎么读?
| 比值 | 含义 | 处理 |
|---|---|---|
| 1 ~ 1.5 | 正常 | 无需处理 |
| > 1.5 | 碎片较多 | 看 allocator_frag_bytes 确认,再考虑整理 |
| < 1 | RSS 比逻辑内存还小 | 发生了 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 key | LFU 访问计数器(需 *-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["serverCron 每 100ms 触发(hz=10)"] --> A["从该库 expires 字典<br/>随机抽 20 个 key"]
A --> D["删掉其中已过期的<br/>未过期的原样保留"]
D --> B{"本轮过期比例 > 25%?"}
B -- "是" --> E{"本轮耗时<br/>超预算了?"}
E -- "否" --> A
E -- "是" --> F["结束本轮"]
B -- "否" --> F
F["等下一个 cron 周期"]
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["maxmemory-policy"] --> B["不淘汰(noeviction)"]
A --> C["在『设置了过期时间』的 key 里淘汰<br/>volatile-*"]
A --> D["在『所有 key』里淘汰<br/>allkeys-*"]
C --> C1["volatile-lru 最近最少使用"]
C --> C2["volatile-lfu 最不经常使用"]
C --> C3["volatile-lrm 最久未修改 8.6+"]
C --> C4["volatile-ttl 剩余寿命最短"]
C --> C5["volatile-random 随机"]
D --> D1["allkeys-lru 最常用"]
D --> D2["allkeys-lfu 热点明显"]
D --> D3["allkeys-lrm 只留最近更新过的 8.6+"]
D --> D4["allkeys-random 访问均匀"]
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{"能接受丢数据吗?"} -- 不能 --> R1["noeviction<br/>(配合持久化/告警)"]
S1 -- 能 --> S2{"所有 key 都能丢吗?"}
S2 -- 能 / 纯缓存 --> S3{"有明显热点吗?"}
S3 -- 有 --> R2["allkeys-lfu<br/>(或 allkeys-lru)"]
S3 -- 没有 --> R3["allkeys-random"]
S2 -- 只有部分能丢 --> S4{"这些 key 都有 TTL 吗?"}
S4 -- 有 --> R4["volatile-lru / volatile-lfu"]
S4 -- 没有 --> R5["先补上 TTL,否则退化成 noeviction"]
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["需要淘汰内存时"] --> B["随机采样 N 个 key<br/>(maxmemory-samples,默认 5)"]
B --> C["比较各 key 的 lru 字段<br/>找到最久未访问的"]
C --> D["淘汰它"]
D --> E["为减少误差,Redis 3.0+ 维护<br/>淘汰池(eviction pool)"]
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 / (lfu-log-factor × counter)——访问越频繁增长越难,避免「新 key 瞬间涨到 255 秒杀老热点」。 - 时间衰减:每过
lfu-decay-time分钟没访问,计数器减 1(下限 5)。让「曾经的王者」重新参与竞争。
lfu-log-factor 10 # 控制"涨得多难",值越大越难增长
lfu-decay-time 1 # 控制"掉得多快",单位分钟
💡 一句话:LRU 看"多久没用"(时间维度),适合访问模式随时间变化;LFU 看"用了多少次"(频率维度),适合有明显稳定热点。新闻热榜用 LFU,用户会话 token 用 LRU。
七、淘汰的完整触发流程
flowchart TD
A["客户端执行命令"] --> B{"这条命令会增加内存吗?"}
B -- "不会(GET/DEL 等)" --> Z["直接执行,不检查"]
B -- "会" --> C["检查 used_memory 是否 > maxmemory"]
C -- 否 --> Z
C -- 是 --> D{"maxmemory-policy"}
D -- "noeviction" --> E["写命令返回 OOM 错误"]
D -- "volatile-*" --> F["从『过期字典』中采样淘汰"]
D -- "allkeys-*" --> G["从『所有 key』中采样淘汰"]
F --> H{"找到可淘汰的 key?"}
G --> H
H -- "否(如 volatile-* 但无 TTL)" --> E
H -- "是" --> I["采样 N 个 → 淘汰最优者"]
I --> J["释放内存"]
J --> C
Z --> K["返回结果"]
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 覆盖写清 TTL | key 永不过期,内存只涨不降 | 统一 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["Redis 内存管理"] --> M1["① 内存构成<br/>used_memory:数据 + 开销 + 缓冲<br/>rss 再加:碎片 + 进程开销"]
ROOT --> M2["② 监控与治理<br/>INFO memory / allocator_frag* / activedefrag"]
ROOT --> M3["③ 过期删除<br/>惰性 + 定期(无定时)"]
ROOT --> M4["④ 内存淘汰<br/>maxmemory-policy:noeviction / volatile-* / allkeys-*"]
ROOT --> M5["⑤ 近似算法<br/>LRU 24位时钟 + 采样;LFU 频率 + 衰减"]
M3 --> K1["关键:惰性兜底正确性,定期兜底回收"]
M4 --> K2["关键:volatile-* 必须配 TTL"]
M5 --> K3["关键:采样近似,samples 默认 5"]
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 看频率,采样近似省开销。
三条通用建议:
- 给缓存 key 都设 TTL,覆盖写用
KEEPTTL或显式带EX,别让 TTL 被悄悄清掉; maxmemory留足余量(物理内存 60%~75%),关掉 swap,控制单实例大小;- 纯缓存用
allkeys-lru/allkeys-lfu,绝不丢数据用noeviction,用volatile-*就必须全员带 TTL。
如果这篇对你有帮助,欢迎点赞收藏,也欢迎在评论区补充你踩过的内存坑 👇