Redis 过期数据的删除和淘汰策略
过期清除策略
redis针对设置过期时间的key,所使用的清除策略有主动和被动两种方式。
flowchart TD
A[Redis 启动] --> B{客户端访问某个 key?}
B -- 是 --> C{该 key 已设置过期时间?}
C -- 是 --> D{当前时间 > 过期时间?}
D -- 是 --> E[立即删除该 key
(惰性删除 Lazy Expire)]
D -- 否 --> F[正常返回 key 的值]
C -- 否 --> F
B -- 否 --> G{后台周期性任务触发
(默认每秒 10 次)}
G --> H[随机采样一批设置了过期时间的 key
(默认 20 个)]
H --> I{采样中发现过期 key?}
I -- 是 --> J[删除所有过期 key]
I -- 否 --> K[本次扫描结束]
J --> L{过期 key 占比 > 25%?}
L -- 是 --> M[立即再次执行定期删除
(避免堆积)]
L -- 否 --> N[等待下一次周期任务]
E --> O[内存释放]
F --> P[业务逻辑继续]
K --> Q[继续监听客户端请求]
M --> H
N --> Q
style E fill:#ffebee,stroke:#f44336
style J fill:#e8f5e9,stroke:#4caf50
style M fill:#fff8e1,stroke:#ff9800
被动检测
被动检测,也称惰性删除。
放任键过期不管,每次从键空间中获取键时,都检查取得的键是否过期,如果过期就删除该键,否则就返回该键值。
被动方式对于那些永远不会再访问的 key 并没有效果。不管怎么,这些 key 都应被过期淘汰,所以 Redis 周期性主动随机检查一部分被设置生存时间的 key,那些已经过期的 key 会被从 key 空间中删除。
sequenceDiagram
participant Client as 客户端
participant Redis as Redis Server
participant KeySpace as 键空间 (keyspace)
participant Expires as 过期字典 (expires)
Client->>Redis: GET mykey
Redis->>Expires: 检查 mykey 是否在 expires 中?
alt 存在过期时间
Expires-->>Redis: 返回过期时间 T
Redis->>Redis: 判断当前时间 > T ?
alt 已过期
Redis->>KeySpace: 从 keyspace 删除 mykey
Redis->>Expires: 从 expires 删除 mykey
Redis-->>Client: 返回 nil
else 未过期
Redis->>KeySpace: 从 keyspace 获取 mykey 的值
Redis-->>Client: 返回 value
end
else 无过期时间
Redis->>KeySpace: 直接获取 mykey 的值
Redis-->>Client: 返回 value
end
主动检测
-
定时删除:在设置键过期时间的同时,创建一个定时器 timer。让定时器在键的过期时间来临时,执行对键的删除操作。
-
定期删除:每隔100ms就对数据库进行一次随机抽查,删除里面的过期键。至于要删除多少过期键,以及要检查多少个数据库,则由算法决定。比如Redis每秒执行10次下面的操作:
- 从带有生存时间的 key 的集合中随机选 20 进行检查。
- 删除所有过期的key。
- 如20里面有超过25%的key过期,立刻继续执行步骤1。
flowchart TD
A[Redis 启动后台定时任务] --> B{每秒执行 10 次
(由 server.hz 控制)}
B --> C[从 expires 字典中
随机抽取 20 个带过期时间的 key]
C --> D{遍历这 20 个 key}
D --> E{当前 key 已过期?}
E -- 是 --> F[从 keyspace 和 expires 中删除该 key]
E -- 否 --> G[保留该 key]
F --> H
G --> H
H{是否遍历完 20 个?}
H -- 否 --> D
H -- 是 --> I{本次扫描中
过期 key 占比 > 25%?}
I -- 是 --> J[立即再次执行本流程
(继续清理)]
I -- 否 --> K[等待下一次周期任务]
style F fill:#e8f5e9,stroke:#4caf50
style J fill:#fff8e1,stroke:#ff9800
数据淘汰策略
Redis 的内存淘汰机制(Eviction Policy)是指当 Redis 的内存使用量达到设定的最大值(通过 maxmemory 配置)时,选择哪些数据被删除以释放空间来存放新数据的策略。
由于redis定期删除是随机抽取检查,不可能扫描清除掉所有过期的key并删除,一些key由于未被请求,惰性删除也未触发。
同时存在未设置过期时间的key,这样redis的内存占用会越来越高。此时就需要内存淘汰机制。
淘汰算法
Redis 的淘汰算法有3种:
-
random(随机)
-
TTL(将要过期)
-
LRU(最近最少使用)
淘汰策略
1 | # 指定 Redis 能使用的最大内存,100MB限制 |
Redis 支持6种淘汰策略:
noeviction(默认):不清除数据,只返回错误,会导致浪费掉更多内存(DEL 命令和其他的少数命令例外)
allkeys-lru:从所有的数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰
allkeys-random:从所有数据集(server.db[i].dict)中任意选择数据淘汰
volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰,ttl值越大越优先被淘汰
flowchart TD
A[客户端发起写操作:SET, HSET, LPUSH等] --> B{当前已用内存大于等于 maxmemory?}
B -- 否 --> C[直接执行写入 返回 OK]
B -- 是 --> D{maxmemory-policy 配置为何值?}
D -->|noeviction| E[拒绝写入 返回错误: OOM command not allowed when used memory > 'maxmemory'.]
D -->|allkeys-lru| F[从 所有 key 中 近似 LRU 淘汰 1 个]
D -->|allkeys-lfu| G[从 所有 key 中 近似 LFU 淘汰 1 个]
D -->|allkeys-random| H[从 所有 key 中 随机淘汰 1 个]
D -->|volatile-lru| I[从 有 TTL 的 key 中 近似 LRU 淘汰 1 个]
D -->|volatile-lfu| J[从 有 TTL 的 key 中 近似 LFU 淘汰 1 个]
D -->|volatile-ttl| K[从 有 TTL 的 key 中 淘汰 TTL 最小(最快过期)的 key]
D -->|volatile-random| L[从 有 TTL 的 key 中 随机淘汰 1 个]
F --> M
G --> M
H --> M
I --> M
J --> M
K --> M
L --> M
M[删除选中的 key 释放内存] --> N[重试原写入操作]
N --> O[写入成功 返回 OK]
classDef error fill:#ffebee,stroke:#f44336,color:#c62828;
classDef success fill:#e8f5e9,stroke:#4caf50,color:#2e7d32;
classDef policy fill:#e3f2fd,stroke:#2196f3;
class E error
class C,O success
class F,G,H,I,J,K,L,M policy
如果没有设置 expire 的key, 那么 volatile-lru, volatile-random 和 volatile-ttl 策略的行为, 和 noeviction基本一致。
一般来说:
设置
expire会消耗额外的内存, 所以使用 allkeys-lru 策略, 可以更高效地利用内存 。 如果某个命令导致大量内存占用(一个很大的set), 在一段时间内, 内存的使用量会明显超过 maxmemory 限制。
策略选择建议
策略使用规则:
如果数据呈现幂律分布,也就是一部分数据访问频率高,一部分数据访问频率低,则使用 allkeys-lru。如果分为热数据与冷数据, 推荐使用 allkeys-lru 策略。 如果不确定具体的业务特征, 那么 allkeys-lru 是一个很好的选择。
如果数据呈现平等分布,也就是所有的数据访问频率都相同,则使用 allkeys-random,即读写所有元素的概率差不多。
| 策略 | 适用场景 |
|---|---|
allkeys-lru |
通用场景,最常用。如果你的数据访问模式大致符合幂律分布(即一部分数据访问频率高,另一部分低),或者你不确定该用什么策略,选这个通常不会错。 |
allkeys-lfu |
你希望数据的访问频率对淘汰决策影响更大时。例如,一个key即使刚被访问过,但如果它历史上只被访问过一两次,而其他key被访问过成百上千次,那么它仍然可能被淘汰。 |
volatile-ttl |
你希望通过设置不同的TTL来主动管理数据的优先级,并且希望Redis优先淘汰那些快要过期的“临时”数据,保留那些TTL很长的核心数据。 |
volatile-lru / volatile-lfu |
你希望将 Redis 既用作缓存(有过期时间),又用作持久存储(无过期时间)。这样能保证永久数据不会被淘汰,只淘汰缓存数据。 |
allkeys-random / volatile-random |
所有数据被访问的概率都差不多,没有明显的热点数据。 |
noeviction |
不推荐用于缓存场景。适用于你希望数据永不被删除,并且内存足够或由应用层处理写失败的情况。 |
