---
title: Redis 过期与淘汰策略
type: concept
domain: redis
tags: [redis, expiration, eviction, lru, lfu, ttl, memory-management]
status: learning
created: 2026-09-20
last_reviewed: 2026-09-20
method: feynman
related_questions:
  - "Redis 的过期策略是怎么样的？"
  - "Redis 的内存淘汰策略是怎么样的？"
related_knowledge:
  - ./cache-three-failures.md
  - ./cache-consistency.md
visualizations: []
anki_cards: 6
interview_rounds:
  - "2026-09-20-round-1-Q5"
  - "2026-09-20-round-1-Q6"
---

# Redis 过期与淘汰策略

> 过期是 TTL 驱动的删除，淘汰是内存驱动的选择性删除——两个独立机制，不是一回事。

## TL;DR（30 秒扫完）

- **过期策略 = 惰性删除 + 定期删除**（组合使用，不是独立线程）
- **淘汰策略 = 8 种 maxmemory-policy**（范围 × 算法的正交矩阵）
- **LRU 是近似的**：采样 5 个候选选最老，不是严格 LRU
- **LFU 是 Redis 4.0+**：15-bit 计数器 + 10 秒时间衰减，防热点长尾
- **默认是 noeviction**：内存满直接写失败，生产必须显式配置

## 关键结论

- **结论 A**：过期扫描是主线程 `serverCron` 定时任务，不是独立线程
- **结论 B**：8 种淘汰策略 = 2 种范围（allkeys/volatile）× 4 种算法（无/随机/LRU/LFU/TTL）
- **结论 C**：LRU/LFU 都是"近似"的——维护链表+Hash 开销大，采样更划算
- **结论 D**：`volatile-*` 策略如果可删 key 不够会返回错误，生产慎用

## 完整讲解（费曼四步）

### STEP 1 · 概念

**过期策略**：如何处理到达 TTL 的 key（Redis 只有两种机制组合）
**淘汰策略**：内存达到 `maxmemory` 时如何腾空间（8 种策略）

两者**独立但会同时发生**（如 `volatile-lru` 既检查 TTL 又检查 LRU）。

### STEP 2 · 大白话

**过期策略比喻**：
- 惰性删除：快递过期了，有人来取才扔掉（访问时才检查）
- 定期删除：物业每天巡楼扔过期快递（主线程定时扫，但只扫一部分）

**淘汰策略比喻**：
- `noeviction`：柜子满了就不放新书（默认，写失败）
- `allkeys-lru`：柜子满了就扔最久没翻的书（全部 key 中淘汰 LRU）
- `volatile-lru`：柜子满了就扔有到期日且最久没翻的书（只在有 TTL 的 key 中淘汰）
- `allkeys-lfu`：柜子满了就扔翻得最少的书（基于访问频率）

### STEP 3 · 底层

#### 过期策略两大机制（组合使用）

**① 惰性删除（Lazy Expiration）**

- 访问 key 时（`lookupKey` 阶段）判断 `expire < now`
- 过期就调 `expireDictGenericDelete` 立即删除
- **优点**：零开销（只有访问才触发）
- **缺点**：冷门 key 永不被访问 → 内存一直占着

**② 定期删除（Periodic Expiration）**

- **主线程定时器 `serverCron`**（每秒约 10 次）触发 `activeExpireCycle`
- 每次从 `expires` 字典随机抽 **20 个**有过期时间的 key
- 统计这 20 个中**实际过期的比例**：
  - 过期比例 ≤ 25%：本轮结束（说明整体过期不多）
  - 过期比例 > 25%：继续循环，直到 CPU 时间超过 **25ms** 或扫完
- **优点**：控制过期 key 数量，避免内存泄漏
- **缺点**：采样有遗漏，个别 key 会晚删

**关键澄清**：不是独立线程，是主事件循环里的定时任务。通过采样 + 时间上限保证不阻塞命令执行。

**为什么不用主动全扫？**
- 单线程全扫会阻塞所有命令，延迟飙升
- 采样 + 时间限制是 CPU 与内存的折中

**实现细节**：
- 过期信息存 `expires` 字典（key → unixtime ms），与 `db` 主字典分离
- `EXPIRE`/`PEXPIRE`/`SET EX` 写 expires 字典
- 每次命令执行前后都可能触发一次 `activeExpireCycle`

#### 内存淘汰策略 8 种（正交矩阵）

| 范围 | 淘汰算法 | 策略名 | 说明 |
|------|---------|--------|------|
| 全部 key | 无 | `noeviction` | **默认**，写失败返回错误 |
| 全部 key | 随机 | `allkeys-random` | 从所有 key 随机删 |
| 全部 key | LRU | `allkeys-lru` | 从所有 key 淘汰最近最少使用 |
| 全部 key | LFU | `allkeys-lfu` | 从所有 key 淘汰最不经常使用 |
| 有过期时间的 key | 随机 | `volatile-random` | 只在有 TTL 的 key 中随机删 |
| 有过期时间的 key | LRU | `volatile-lru` | 只在有 TTL 的 key 中淘汰 LRU |
| 有过期时间的 key | LFU | `volatile-lfu` | 只在有 TTL 的 key 中淘汰 LFU |
| 有过期时间的 key | TTL | `volatile-ttl` | 优先淘汰剩余 TTL 最小的 |

**范围维度**：
- `allkeys-*`：无论有没有过期时间都参与淘汰（缓存场景推荐）
- `volatile-*`：只在**设置了 EXPIRE** 的 key 中淘汰；如果这类 key 不够删，就返回错误

#### LRU 是「近似 LRU」——非严格 LRU

- 严格 LRU 需要维护双向链表 + Hash，开销大
- Redis 用**采样淘汰**：随机抽 `maxmemory-samples`（默认 5）个候选，选最老淘汰
- 默认采样 5 个误差约 1%，可配置 10-100 提升精度但增加开销
- 时间戳存在 key 的 `lastAccessedTime` 字段（4 字节）

#### LFU 是「近似 LFU」——Redis 4.0+ 新增

- **15-bit 计数器**存在 key 结构里，记录访问频率
- 计数器采用**时间衰减**：每 10 秒（`lfu-log-freq-factor` 控制）衰减一次
- 新 key 首次访问给初始值（默认 5）
- **优势场景**：热点分布长尾时，防止新数据挤走真正的热点

#### noeviction 的坑

- 默认策略，内存满后所有写操作返回 `OOM command not allowed when used memory exceeds 'maxmemory'`
- 生产环境必须显式配置淘汰策略，不能让默认值扛
- 业务层要处理这个错误（重试/降级）

#### 选型建议

| 场景 | 推荐策略 | 原因 |
|------|---------|------|
| 通用缓存 | `allkeys-lru` | 有 TTL 无 TTL 一起淘汰，不挑 |
| 热点长尾 | `allkeys-lfu` | 防新数据挤走真正热点 |
| 需要保留数据 | `volatile-lru` | 只淘汰有 TTL 的，但可能返回错误 |
| 会话缓存 | `volatile-ttl` | 优先淘汰剩余 TTL 最小的 |
| 数据安全优先 | `noeviction` | 写失败好过丢数据，但业务要处理 |

#### 监控指标

```
INFO stats:
  evicted_keys: 被淘汰的 key 数（内存驱动）
  expired_keys: 过期的 key 数（TTL 驱动）

CONFIG GET maxmemory-policy  # 查当前策略
```

### STEP 4 · 简化

**一句话总结**：过期=惰性+定期（主线程定时扫），淘汰=8 种策略（范围×算法），LRU/LFU 都是近似的。

**记忆口诀**：
- 过期：访问删 + 定时扫（20 个/25%/25ms）
- 淘汰：8 种 = 2 范围（allkeys/volatile）× 4 算法（无/随机/LRU/LFU/TTL）
- LRU：采样 5 个选最老
- LFU：15-bit 计数器 + 10 秒衰减
- 默认：noeviction（写失败）

## 常见误区

- **误区 1**：说"Redis 有专门的过期线程" → 正确：是主线程 `serverCron` 定时任务
- **误区 2**：说"过期 key 立即删除" → 正确：不保证，采样机制有概率延迟
- **误区 3**：说"LRU 是严格 LRU" → 正确：是采样的近似 LRU，维护链表开销大
- **误区 4**：说"LFU 从 3.0 就有" → 正确：Redis 4.0 新增
- **误区 5**：说"淘汰和过期是一回事" → 正确：过期是 TTL 驱动，淘汰是内存驱动，两个独立机制
- **误区 6**：说"默认策略是 allkeys-lru" → 正确：默认是 `noeviction`

## 延伸追问

1. **`activeExpireCycle` 的具体逻辑？**
   - 采样 20 个过期 key，过期比例 >25% 继续，单次最多 25ms；`serverCron` 每秒 10 次 + 命令前后触发
2. **如何判断过期 key 堆积？**
   - `INFO keyspace` 看 `expired_keys`；`MEMORY USAGE` 看内存；`DBSIZE` 与真实存活数对比
3. **`volatile-lru` 和 `allkeys-lru` 怎么选？**
   - 有 TTL 的 key 占比高用 volatile-lru；混合场景用 allkeys-lru
4. **`evicted_keys` 突增如何排查？**
   - `INFO memory` 看 used_memory vs maxmemory；`INFO stats` 看 evicted_keys 速率；确认是策略主动淘汰还是内存爆表
5. **Redis 7.0 对过期清理有什么优化？**
   - 过期清理的 CPU 时间控制更精细；`activeExpireCycle` 支持按数据库独立触发
6. **`maxmemory-samples` 怎么调优？**
   - 默认 5 误差约 1%；提高到 10-100 精度提升但开销增大；一般不改

## 速查表

```
过期: 惰性删除 + 定期删除（主线程 serverCron 定时扫）
定期扫: 20 个/次, 过期率 >25% 继续, 单次最多 25ms
淘汰: 8 种 = 2 范围(allkeys/volatile) × 4 算法(无/随机/LRU/LFU/TTL)
LRU: 近似, 采样 maxmemory-samples(默认 5) 个选最老
LFU: 近似, 15-bit 计数器 + 10 秒衰减, Redis 4.0+
默认: noeviction（写失败）
监控: INFO stats → evicted_keys / expired_keys
```

## 关联题目（题库）

- 《Redis 的过期策略是怎么样的？》— 2026-09-20 Round 1 Q5, ⭐⭐（误说独立线程，遗漏采样细节）
- 《Redis的内存淘汰策略是怎么样的？》— 2026-09-20 Round 1 Q6, ⭐⭐⭐⭐☆（框架正确，缺策略名称）

## 关联知识

- [缓存三失效（穿透/击穿/雪崩）](./cache-three-failures.md)
- [缓存一致性](./cache-consistency.md)
- [Redis 为什么快 + 为什么单线程](./redis-single-thread.md)

## Anki 候选卡片

1. **正**：Redis 过期策略的两个机制？**反**：惰性删除 + 定期删除（组合使用）
2. **正**：定期删除的采样参数？**反**：20 个/次，过期率 >25% 继续，单次最多 25ms
3. **正**：Redis 内存淘汰策略有几种？**反**：8 种 = 2 范围（allkeys/volatile）× 4 算法（无/随机/LRU/LFU/TTL）
4. **正**：LRU 是严格 LRU 吗？**反**：不是，是采样的近似 LRU，默认抽 5 个候选选最老
5. **正**：LFU 从哪个版本开始支持？**反**：Redis 4.0，15-bit 计数器 + 10 秒时间衰减
6. **正**：Redis 默认淘汰策略？**反**：noeviction（内存满写失败返回错误）

---

*最后更新：2026-09-20*
