Redis 系列(六):过期、内存淘汰与内存优化——让 Redis 活得更久

核心目标 :理解 TTL 过期删除的两种机制(惰性 + 定期)及其代价;掌握 maxmemory 八种淘汰策略的语义与选型;会用 MEMORY USAGE/MEMORY DOCTOR/--bigkeys/INFO memory 定位大 key 与内存问题;理解"淘汰导致的缓存雪崩"风险。

前置知识 :完成 Part 2(键空间与 TTL)与 Part 3(每键固定开销 ≈ 50-60 字节)。

验证环境:Redis 8.10.0(cygwin 移植版,主实例 6379 做过期/内存实验,隔离实例 6380 做淘汰策略实验)、redis-py 8.1.0、Python 3.11.6、Windows 11。最后复核日期:2026-08-07。


0. 本篇问题场景:两个"明明设置了却还是出事"

  1. 内存还在涨 :每个键都设了 TTL,为什么 INFO memoryused_memory 还是居高不下?
  2. 缓存雪崩:某个时间点大量缓存同时失效,数据库瞬间被打挂------"过期时间"本身怎么成了事故源?

第一个问题的答案在"过期删除的懒惰性":TTL 到点 ≠ 立即释放内存。第二个问题的答案在"淘汰/过期的同步性":没有打散的过期时间会让热点同时消失。本篇把过期、淘汰、内存三者串成一条线。


1. 过期机制:TTL 到点后发生了什么

1.1 两种删除策略

Redis 对过期键的删除不是到点立刻删除,而是两条路径配合:

策略 机制 代价
惰性删除 每次访问键时检查是否过期,过期则删除 过期但从未再被访问的键会一直占内存
定期删除 后台任务(serverCron,hz 频率)抽样扫描过期键并删除 扫描有成本,且是抽样不是全量

惰性删除保证"读到的永远是活的";定期删除兜底"没人访问的过期键也要清理"。两者都不是"到点秒删"。

1.2 实测一:惰性删除

text 复制代码
$ redis-cli SET lazy_key "v" EX 1
OK
$ redis-cli GET lazy_key        # 1 秒内:v
v
(等待 2 秒)
$ redis-cli GET lazy_key        # 已过期,访问时被惰性删除
(空)
$ redis-cli EXISTS lazy_key
0

1.3 实测二:定期删除(activeExpireCycle)

写入 1 万个 2 秒 TTL 的键后不做任何访问 ,观察 INFO statsexpired_keysdbsize

text 复制代码
写入 10000 个 2 秒 TTL 键, dbsize: 10000
(等待 5 秒,期间无任何客户端访问)
expired_keys: 1 -> 10001   (定期删除实际删除了 10000 个;起始值 1 来自此前惰性删除实验的计数)
5 秒后 dbsize: 0

结论 :定期删除是真实生效的------即使没人访问,过期键也会被后台抽样扫描清理。但它是抽样 + 分批 的:如果同一时刻到期键特别多(秒杀场景、批量设置相同 TTL),单次扫描清不完,used_memory 会在一段时间内"虚高"。这就是"设置了 TTL 内存还在涨"的机制解释:删除是异步的、有节奏的,不是即时的。

1.4 过期键对持久化与复制的影响

  • RDB:生成快照时已过期的键不写入;加载时已过期的键被跳过;
  • AOF :键过期后追加一条 DEL,保证重放后一致;
  • 主从复制 :过期删除只由主库发起 ,从库不主动删,靠主库同步 DEL(Part 10 展开)------从库读到已过期但未收到 DEL 的键时返回空值(replica-serve-stale-data 相关)。

2. maxmemory 与八种淘汰策略

2.1 什么是淘汰

used_memory 达到 maxmemory 上限时,Redis 按 maxmemory-policy 决定"拒绝写"还是"淘汰键腾空间"。八种策略按两个维度划分:

维度 含义
allkeys-* 所有键里挑淘汰对象
volatile-* 只从设置了 TTL 的键里挑;没有可淘汰的 TTL 键时退化为 noeviction
lru / lfu / random / ttl 淘汰依据:最近最少用 / 最不经常用 / 随机 / 剩余 TTL 最短

2.2 实测一:noeviction------写满即拒绝

隔离实例 maxmemory 16mbmaxmemory-policy noeviction,写入 4 个 4MB 键后:

text 复制代码
写 big:0 (4MB): True
写 big:1 (4MB): True
写 big:2 (4MB): True
写 big:3 (4MB): True
写 big:4 报错: command not allowed when used memory > 'maxmemory'.
evicted_keys: 0

noeviction 是"宁可拒绝写入也不丢数据"------适合把 Redis 当数据存储的场景;缓存场景用它会在写满时直接报错,影响业务。

2.3 实测二:allkeys-lru------淘汰最久未用

同一配置改 allkeys-lru,先写 4 个 4MB 键,访问 big:1 激活它,再继续写 2 个:

text 复制代码
访问 big:1(激活 LRU): aaaaa
继续写 big:4、big:5 后 evicted_keys: 3
存活: ['big:1', 'big:4', 'big:5']
淘汰: ['big:0', 'big:2', 'big:3']

LRU 语义完全符合预期:被访问过的 big:1 存活,最久未用的 big:0/2/3 被淘汰

2.4 实测三:volatile-lru------只动带 TTL 的键

写入 3 个带 TTL 的键 + 1 个永久键(permanent),继续写 2 个带 TTL 键:

text 复制代码
继续写 2 个带 TTL 键后 evicted_keys: 2
存活: ['permanent', 'volatile:3', 'volatile:4']
淘汰: ['volatile:0', 'volatile:1', 'volatile:2']
permanent 是否存活: 1

永久键在内存压力下毫发无损 ------volatile-* 系列绝不碰没有 TTL 的键。注意它的陷阱:如果带 TTL 的键很少或都被访问过,可能"无键可淘汰",退化为 noeviction 直接报错。

2.5 LRU 的近似实现与 LFU

严格 LRU 需要维护全量访问时间排序,成本太高。Redis 用近似 LRUlru 字段(Part 3 的 24 bit)记录访问时间,淘汰时抽样 若干键(默认 maxmemory-samples 5)淘汰其中最旧的。代价是"近似"------抽样可能漏掉真正的冷数据。

LFU(allkeys-lfu/volatile-lfu)记录的是访问频次 而非最近时间:lru 字段的低 8 位存频次、高 16 位存衰减时间,频次会随时间衰减。适合"访问频率比访问时间更能代表热度"的场景(如热点商品)。

选型决策树

text 复制代码
Redis 承载的是数据(不能丢)?→ noeviction + 监控告警
缓存场景?
  ├─ 所有键都有 TTL?→ volatile-lru / volatile-ttl
  ├─ 有永久热点键?   → allkeys-lru(避免永久键占满)
  └─ 频率比时间重要? → lfu 系列
无法接受任何淘汰?     → 扩容 maxmemory / 缩键

3. 淘汰的连锁反应:缓存雪崩

淘汰不是免费的 :被淘汰的键下次访问会回源数据库。三种常见事故:

事故 触发 后果
缓存雪崩 大量键同一时刻过期/被淘汰 同时回源,数据库打挂
缓存穿透 查询不存在的 key 每次都回源(Part 8 详讲)
缓存击穿 单个热点 key 过期瞬间 高并发同时重建(Part 8 详讲)

本系列的防偏原则:淘汰/过期参数要与回源能力匹配 ------maxmemory 设得过小 + allkeys-lru,会在流量高峰把整批缓存"洗掉",雪崩随之而来。缓解手段(Part 8 完整展开):过期时间加随机抖动、多级缓存、限流降级、热点预加载。


4. 内存分析与大 key 治理

4.1 四个工具

text 复制代码
$ redis-cli MEMORY USAGE key            # 单键内存(value 部分)
$ redis-cli MEMORY USAGE key 0          # 含 key 名
$ redis-cli MEMORY DOCTOR               # 内存健康体检
$ redis-cli --bigkeys                   # 采样扫描最大的 key

实测 --bigkeys 在 2 万小键 + 1 个 5MB 大键 + 1000 个 List 上的输出:

text 复制代码
[04.55%] Biggest string found so far "big:value" with 5000000 bytes
-------- summary -------
Biggest   list found "list:333" has 200 items
Biggest string found "big:value" has 5000000 bytes

4.2 INFO memory 关键指标

text 复制代码
used_memory:20,689,174        # 数据实际占用(不含碎片)
used_memory_human:19.73M
used_memory_peak_human:367.87M  # 历史峰值------分配器不归还峰值内存
mem_fragmentation_ratio:1.00    # 碎片率:>1.5 需关注
maxmemory_human:0B              # 0 = 未设上限
maxmemory_policy:noeviction

used_memory_peak 是最容易被忽略的指标:分配器(jemalloc/malloc)在内存峰值后通常不会把空间归还给 OS ------即使数据删了,RSS 也回不到峰值以下。MEMORY DOCTOR 的实测提示正是这一点:

text 复制代码
Sam, I detected a few issues in this Redis instance memory implants:
 * Peak memory: In the past this instance used more than 150% the memory that
   is currently using. The allocator is normally not able to release memory
   after a peak ...

4.3 大 key 的四类危害

危害 机制
阻塞 单线程下大 value 的读写/序列化占用命令执行时间
带宽 GET/LRANGE 全量传输大对象,拖慢客户端与网络
主从延迟 大 key 的增量同步传输放大延迟(Part 10)
集群倾斜 大 key 所在分片内存/流量失衡(Part 11)

治理手段--bigkeys/--memkeys 定期扫描 → 拆 key(按字段/按桶)→ 压缩序列化 → 必要的大对象移出 Redis 存对象存储。


5. 内存优化手段

5.1 键粒度是最大的杠杆

Part 3 实测:每个小键固定开销 ≈ 55.6 字节,与 value 大小无关。所以:

text 复制代码
10 万个小键(value 1B) ≈ 5.6 MB,其中 98% 是包装开销

同样的数据合并成一个 Hash 的 10 万个字段,内存能省 10 倍以上。优化顺序:先看键数量,再看 value 大小------"把多个字段拆成独立 String 键"往往是内存翻倍的开端。

5.2 小对象编码参数

Part 4 的 listpack/intset 编码把大量小对象压进紧凑结构。相关参数(CONFIG GET 实测本机默认):

text 复制代码
hash-max-listpack-entries 512
set-max-intset-entries    512
set-max-listpack-entries  128
zset-max-listpack-entries 128

调大这些阈值可以推迟 hashtable/skiplist 升级、保持紧凑编码------但代价是字段级读写变慢(listpack 更新要移动内存)。这是"内存 vs CPU"的权衡,不是越大越好。

5.3 碎片与共享

  • mem_fragmentation_ratio(used_memory_rss / used_memory)> 1.5 时考虑 activedefrag yes 或重启(分配器归还峰值内存,见 4.2);
  • 小整数共享(Part 3 实测本机 8.10 未生效)------版本相关,不可依赖。

6. 版本与环境差异

差异点 官方 7.4 本机 8.10.0(cygwin 移植版) 影响
过期删除机制 惰性 + 定期(hz=10) 一致 本机 hz:10 实测(Part 1)
maxmemory 策略 八种 八种,行为一致 本文三个实验均为本机实测
共享整数对象 0-9999 实测未生效(Part 3) 依赖共享省内存的结论需复核版本
--bigkeys 支持 支持 输出格式一致

淘汰/过期行为在 7.x/8.x 上稳定,本文实验数据可直接复用。


7. 测试与验收

  • 建议测试:过期后 GET 返回 None(惰性)、批量短 TTL 后 dbsize 归零(定期)、maxmemory 写满的三种策略行为(隔离实例)、--bigkeys 能发现构造的大 key;
  • 淘汰实验必须在隔离实例做,不要在生产/共享实例上压内存。

本篇验收清单

  • 能解释"TTL 到点 ≠ 立即释放内存"并说出两种删除策略;
  • 能说出 noeviction/allkeys-lru/volatile-lru 的语义差异,并解释为什么 volatile 系列可能退化为 noeviction;
  • 能解释近似 LRU 与 LFU 的差异及 maxmemory-samples 的作用;
  • 会用 MEMORY USAGE/MEMORY DOCTOR/--bigkeys/INFO memory 四件套定位内存问题;
  • 能解释"键数量比键大小更费内存"及 Hash 合并的优化思路;
  • 能设计 maxmemory 与淘汰策略的选型(数据 vs 缓存 vs 热点)。

8. 常见误区

  1. "TTL 到点内存立刻释放"------惰性 + 定期两种机制都有延迟(§1)。
  2. "volatile-lru 永不报错"------没有带 TTL 的键可淘汰时退化为 noeviction,照样 OOM(§2.4)。
  3. "allkeys-lru 会把我的永久数据删掉"------会!它是缓存策略,承载业务数据时用 noeviction 或 volatile 系列。
  4. "删了数据 RSS 就会降"------分配器不归还峰值内存,重启才可能回收(§4.2)。
  5. "maxmemory 越大越好"------上限越接近物理内存越危险:峰值 + 碎片 + COW 会让实例 OOM 崩溃。
  6. "--bigkeys 能发现所有大 key" ------它是采样扫描,漏检率随数据量上升;精确分析用 MEMORY USAGE 或离线分析 RDB。

9. 本篇小结

回到开篇两个问题:

  1. "设置了 TTL 内存还在涨" :过期删除是惰性 + 定期两条异步路径,批量过期时清理有节奏;叠加"分配器不归还峰值内存",内存下降总是慢于直觉------需要看 expired_keysused_memory_peak 而不是只看 used_memory
  2. "缓存雪崩从哪来" :淘汰/过期策略没有与回源能力匹配------maxmemory 过小、过期时间无抖动、热点无预加载,都是雪崩的引信。

本篇建立的治理模型:过期管"键的生命周期",淘汰管"内存上限的兜底",优化管"每键成本"------三层配合,Redis 才能在有限内存里稳定运转。

下一篇进入并发与架构篇:Part 7:事务、Pipeline 与 Lua 脚本,回答"Redis 的原子性到底是什么",用秒杀扣库存把 MULTI/WATCH/Lua 串起来。


10. 官方资料

相关推荐
杜子不疼.1 小时前
模型效果不好时,我现在会先看数据集
数据库
夜雪一千1 小时前
MySQL的事务是什么?原理、ACID、隔离级别与实战详解
数据库·mysql
上下求索,莫负韶华2 小时前
笔记-数据库事务和java事务和切面
java·数据库·笔记
黄华SJ520it2 小时前
顶俏洗衣液模式制度开发介绍:S2B2C社交分销+多门店核销系统全解析
前端·数据库·小程序·零售·系统开发
大黄说说2 小时前
Android 本地存储深度对比:SharedPreferences、MMKV、Room 数据库怎么选?
android·数据库
资讯第一线2 小时前
SQL Server工具:WinToolsPlus 之 SQL Server 日志清理/Suspect/质疑/置疑/可疑/单用户等 修复
数据库·oracle
追风少年ii2 小时前
课前准备--蛋白质PDB文件与PDBQT文件的格式与区别
数据库·数据分析·分子动力学·分子对接
hopsky3 小时前
《HBase 权威指南》 Lars George
大数据·数据库·hbase
Architect_Lee3 小时前
mac快速安装mysql
数据库·mysql·macos