刚学完 Redis 的内存管理和过期淘汰策略,知识点又多又碎:
used_memory和used_memory_rss到底差在哪?为什么 Redis 不用"定时删除"?volatile-lru和allkeys-lru只差一个前缀,效果却天差地别;maxmemory-policy到底该选哪个?这篇文章我把内存从哪来 → 怎么管 → 过期怎么删 → 满了怎么淘汰 串成一条线,配图 + 示例 + 面试追问,一次讲清楚。建议收藏,配合
redis-cli边看边敲。
一、先搞懂:Redis 的内存到底花在哪了?
很多人以为"我存了 1GB 数据,Redis 就用 1GB 内存",这是最大的误解。先记住一条底线:
used_memory= Redis 通过自己的分配器(zmalloc)申请到的字节数 ;used_memory_rss= 操作系统看到这个进程真正占着的物理页 。 两者不是包含关系 :used_memory里的东西未必都在 RSS 里,RSS 里的东西也未必记在used_memory上。
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 碎片是怎么产生的
典型场景 :你不停地 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)
实现位置:expireIfNeeded(),在读写命令真正执行前调用。
缺点 :如果一个 key 过期后再也没人访问,它就永远不会被删除,内存一直被占。所以必须靠定期删除来兜底。
4.2 定期删除(Active Expiration)
Redis 由 serverCron 驱动,默认每秒 10 次 (hz=10,即每 100ms 一轮),逻辑大致是:
核心参数:
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 种)
| 策略 | 淘汰范围 | 淘汰规则 | 适用场景 | 版本 |
|---|---|---|---|---|
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 天回绕**的相对时钟。
💡 关键点 :淘汰比较的是「快照值的差值」,所以这个时钟回绕是安全的------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 |
+---------------------------+---------------+
两个精妙设计:
-
概率递增(对数递增) :计数器不是每次访问都 +1,而是访问越频繁,增长越难 ------递增概率约为
1 / (lfu-log-factor × counter)。- 计数器为 0 时,+1 的概率是 1;
- 计数越大,+1 概率越低。
- 目的:避免"新 key 因频繁访问瞬间涨到 255,秒杀老热点"。
-
时间衰减 :每过
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 更好(谁最近登录谁活跃)。
七、淘汰的完整触发流程
把前面串起来,一次内存淘汰的完整链路是:
注意:
- 淘汰发生在命令执行前 ,而且是循环 进行的------每淘汰一个 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,监控要单独设阈值。如果手动把它关掉,从库就会独立淘汰,主从数据立刻分叉。
如果这篇对你有帮助,欢迎点赞收藏,也欢迎在评论区补充你踩过的内存坑 👇