CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Redis 缓存入门/课时/过期与淘汰
RawBlame
课时目录12 节

Redis 缓存入门

  1. 01Redis 是什么
  2. 02CLI 与键命名
  3. 03String 与计数
  4. 04Hash / List / Set / ZSet
  5. 05过期与淘汰
  6. 06缓存模式
  7. 07Spring Data Redis
  8. 08会话与 Sa-Token
  9. 09Docker 运行 Redis
  10. 10Pub/Sub 入门
  11. 11测试与监控
  12. 12综合练习

过期与淘汰

EXPIRE、TTL、缓存雪崩/穿透概念。

25 分钟

TTL 与过期淘汰:数据的保质期

这一课你将能做到

  1. 熟练使用 EXPIRE/PEXPIRE/PERSIST,并准确解释 TTL 返回 -1 和 -2 的区别
  2. 说清 Redis 删除过期 key 的两条路径(惰性删除 + 定期删除),以及内存满时 8 种淘汰策略怎么选
  3. 用「TTL 抖动 + 空值缓存 + 互斥重建」三个手段讲清并预防缓存穿透、击穿、雪崩

为什么要学

给缓存设过期时间,看似一行 EXPIRE 的事,实际上藏着三个经典生产事故:

  • 雪崩:500 个缓存 key 设了同一时刻过期,那一秒所有请求打到 MySQL,数据库直接被压垮
  • 击穿:某个热点 key 过期瞬间,上千并发同时 miss,全部涌向数据库重建同一份缓存
  • 穿透:有人恶意请求根本不存在的 id,缓存里永远没有,每次都打到数据库

这三个问题的解药全部和 TTL 设计有关。本站第 12 课的实战会逐条落地这些策略,这一课先把原理和命令打牢。另外本站限流器「首次 INCR 时才 EXPIRE」的设计,也只有理解 TTL 机制才能看懂。

先建立图像

每个 key 可以挂一个「电子保质期标签」:

SET coderoadmap:tracks:list '...' EX 300
                       │
                       └─ 300 秒后标签归零

TTL 返回值的三种状态:
  ≥ 0   → 还剩多少秒
  -1    → key 存在,但永不过期(缓存场景的危险信号)
  -2    → key 不存在

「过期了」不等于「立刻从内存消失」。Redis 用两条路径清理过期 key:

惰性删除:下次有人摸这个 key 时顺手删(长期没人摸 → 一直占内存)
定期删除:后台周期性随机抽检一批 key,删掉已过期的(抽样,扫不全)
两条路径都不保证及时 → 内存仍可能一直涨 → 需要淘汰策略兜底

人话:TTL 像「便签上的保质期」。保质期全印成同一天,仓库(MySQL)会在那天被挤爆。

跟着做

步骤 1:TTL 三态实验

docker exec -it redis-dev redis-cli

SET demo:a "1" EX 30
TTL demo:a        # (integer) ≥0,倒计时
SET demo:b "2"
TTL demo:b        # (integer) -1 —— 存在但永不过期
TTL demo:nope     # (integer) -2 —— 不存在
PERSIST demo:a    # 摘掉保质期
TTL demo:a        # (integer) -1
EXPIRE demo:a 10  # 再挂 10 秒

步骤 2:观察「过期 ≠ 瞬间删」

SET demo:c "3" EX 2
# 等 3 秒
EXISTS demo:c          # (integer) 0 —— 业务视角已不存在
MEMORY USAGE demo:c    # (nil) —— 空间已被回收或将被回收

Redis 不维护全局倒计时表,过期 key 在被访问或被抽检前可能仍占内存——这就是需要内存淘汰策略兜底的原因。

步骤 3:看淘汰策略配置

CONFIG GET maxmemory-policy
CONFIG GET maxmemory

默认 noeviction(内存满后所有写命令报错)。8 种策略一览:

noeviction            内存满,写命令报错(默认)
allkeys-lru           全库范围 LRU 淘汰 ← 纯缓存实例首选
volatile-lru          只在设了 TTL 的 key 里 LRU
allkeys-lfu           全库 LFU(按访问频率)
volatile-lfu          带 TTL 的 key 里 LFU
allkeys-random        全库随机淘汰
volatile-random       带 TTL 的 key 里随机
volatile-ttl          优先淘汰剩余 TTL 最短的

纯缓存实例的标准搭配:CONFIG SET maxmemory 512mb + CONFIG SET maxmemory-policy allkeys-lru。

步骤 4:TTL 抖动防雪崩(Java 片段)

错误示范 vs 正确示范:

// 错:所有实例同一时刻写入同一 TTL → 到点集体失效
stringRedisTemplate.opsForValue().set(key, json, Duration.ofMinutes(5));

// 对:基础 TTL + 随机抖动
long ttl = 300 + ThreadLocalRandom.current().nextLong(60);
stringRedisTemplate.opsForValue().set(key, json, Duration.ofSeconds(ttl));

第 12 课 TrackCacheService 的 listTtl() 就按这个写。

步骤 5:穿透与击穿的防御演练

模拟穿透——请求一个不存在的课程 slug:

# 缓存空值:把「查过、不存在」也缓存下来,配短 TTL
SET coderoadmap:track:no-such-slug "" EX 60

应用层约定:读到空字符串表示「已知不存在」,与「缓存未命中」(nil)区分开。

模拟击穿的互斥重建——多并发 miss 时,只有一个线程能抢到锁去查库:

SET mutex:track:no-such-slug "1" NX EX 10    # 抢到的线程回源重建
# 其余线程:等一小会儿重试读缓存,或直接返回默认值

讲清楚要点

  1. EXPIRE 是挂在 key 上的独立属性,重写 value 会清掉它:再次 SET key value(不带 EX)会把 TTL 重置为永不过期——「更新值把过期时间弄丢了」是最常见的缓存永生事故。所以 Java 里永远用带 Duration 的 set 重载。
  2. 惰性 + 定期删除保证「过期 key 终将被清」但不保证准时;业务上别依赖「过期那一纳秒 key 就没了」,击穿防护要自己做。
  3. 淘汰策略是内存上限的兜底,不是过期机制的一部分:缓存实例配 allkeys-lru;会话 + 缓存混布实例要谨慎——会话 key 被 LRU 挤掉等于用户被踢下线,要么容量给足加监控,要么分实例部署。
  4. 雪崩解法 = TTL 抖动(错开过期时刻)+ 多级缓存(Caffeine 顶在 Redis 前面,本站已有)。
  5. 击穿解法 = 互斥重建(NX 锁让一个线程回源)或逻辑过期(值里带过期字段,后台异步刷新)。
  6. 穿透解法 = 缓存空值短 TTL + 入参校验,进阶再上布隆过滤器。

你可能会懵的地方

  • 过期删除和淘汰策略有什么区别? 过期删除处理「到了保质期的 key」(惰性 + 定期两条路径);淘汰策略处理「内存超上限时留谁删谁」。两条独立机制,TTL 管新鲜度,淘汰管容量。

  • 为什么 Redis 不做到期即删? 精确到期即删需要维护海量定时器,代价太大。惰性 + 定期是 CPU 和内存之间的工程折中。

  • 本站会话 key 会被 allkeys-lru 挤掉吗? 会——如果 maxmemory 太小、缓存 key 把内存占满。所以会话敏感的实例要么缓存/会话分开部署,要么容量规划 + 监控(第 11 课讲 INFO)。这是「数据分级」思想的落地。

  • TTL 抖动区间怎么定? 经验值:基础时长的 10%20%(5 分钟 TTL 抖 3060 秒)。目的只是让过期时刻错开,不需要精确计算。

  • 缓存空值会不会把垃圾存满 Redis? 用短 TTL(30~120 秒)控制生命周期,攻击停止后自动清空;同时配入参校验拦截明显非法的 id。

错法对照

错法问题正解
SET 更新缓存值不带 EXTTL 被清,key 永生每次写入都带 Duration
全部 key 同一 TTL雪崩300 + random(60) 抖动
miss 后所有线程都回源击穿打爆 MySQLNX 互斥重建 / 逻辑过期
不存在的数据不缓存穿透空值短 TTL + 入参校验
生产不设 maxmemory 用默认 noeviction内存满写入全挂maxmemory + allkeys-lru
缓存空值设成永不过期新数据永远「不存在」空值必须短 TTL

卡点急救

  1. TTL 显示 -1:SET 时漏了 EX,或更新时覆盖了 TTL。补 EXPIRE,并检查代码是否统一走带 Duration 的写入。
  2. 写 Redis 报 OOM command not allowed:maxmemory 打满且策略是 noeviction。先 CONFIG GET maxmemory* 确认,再清理或改策略。
  3. CONFIG SET 改了重启就丢:CONFIG SET 只运行时生效,要持久化用 CONFIG REWRITE(写回配置文件);容器部署更推荐把参数写进启动命令。
  4. key 明明到期了还在:惰性删除还没触发,占内存但不影响正确性(EXISTS 返回 0)。
  5. MySQL 监控出现周期性尖峰:对照 Redis 里大批 key 的写入时刻 + TTL,典型的雪崩节律;上抖动。

面试常问

  • 缓存穿透、击穿、雪崩分别是什么,怎么解决? 穿透 = 查不存在的数据(空值缓存短 TTL、入参校验、布隆过滤器);击穿 = 热点 key 过期瞬间并发回源(互斥重建、逻辑过期);雪崩 = 大批 key 同时过期或 Redis 宕机(TTL 抖动、多级缓存、高可用)。三个词对应三种「请求绕过缓存打到 DB」的模式。

  • Redis 过期 key 是怎么删的? 惰性删除(访问时检查)+ 定期删除(后台随机抽检),两者结合在 CPU 和内存之间折中。所以「刚过期」的 key 可能还占内存。

  • 内存淘汰策略有哪些?你们用哪种? 8 种,默认 noeviction。纯缓存实例用 allkeys-lru;注意会话和缓存混布时 LRU 可能挤掉会话(用户被踢),要容量规划或分实例——本站会话实例靠容量 + 监控保障。

自测三问

  1. TTL 返回 -1 和 -2 分别什么意思?PERSIST 之后 TTL 变成什么?
  2. 为什么「再次 SET 不带 EX」会弄丢过期时间?工程上怎么从代码层面杜绝?
  3. 穿透、击穿、雪崩各自的一句话解法是什么?分别对应哪类「打爆 DB」的模式?

小结一句话

TTL 是缓存的生命线:写入必带过期、TTL 加抖动防雪崩、空值短缓存防穿透、互斥重建防击穿,内存上限靠淘汰策略兜底。

延伸

  • EXPIRE 命令
  • 内存淘汰策略
  • 下一课:Cache-Aside 缓存模式——把缓存、TTL、失效串成完整读写路径,并看懂本站现成的 Caffeine 一层缓存。
加载本节练习…
← 上一课Hash / List / Set / ZSet下一课 →缓存模式