Redis 的 数据过期删除策略 是通过 惰性删除 和 定期删除 两种机制结合实现的,目的是在 内存占用 和 CPU资源消耗 之间取得平衡。以下是详细说明:
一、过期删除策略的核心机制
1. 惰性删除(Lazy Expiration)
- 触发时机:当客户端尝试访问某个键(如
GET
、SET
等操作)时,Redis 会检查该键是否已过期。 - 实现逻辑:
- 如果键已过期,直接删除该键并返回
NIL
(空值)。 - 如果未过期,返回键的值。
- 如果键已过期,直接删除该键并返回
- 优点:
- CPU友好:仅在访问键时检查过期,避免不必要的 CPU 开销。
- 缺点:
- 内存浪费:过期但未被访问的键会一直占用内存,可能导致内存泄漏。
2. 定期删除(Periodic Expiration)
- 触发时机:由 Redis 的周期性任务
serverCron
主动触发(默认每 100ms 执行一次)。 - 实现逻辑:
- 分批次处理:从所有数据库中随机选取部分设置了过期时间的键,检查并删除过期键。
- 限制条件:
- 每次最多检查 20 个键(
ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP
)。 - 单次删除操作的时间不超过 25ms(SLOW 模式)或 1ms(FAST 模式)。
- 每次最多检查 20 个键(
- 优点:
- 减少内存浪费:主动清理过期键,避免惰性删除的内存堆积问题。
- 缺点:
- CPU消耗:随机检查和删除操作会占用一定的 CPU 资源。
二、两种策略的协同工作
Redis 同时使用惰性删除和定期删除,两者互补以优化性能:
- 惰性删除 处理客户端直接访问的键,确保及时清理过期数据。
- 定期删除 负责清理未被访问的过期键,防止内存浪费。
三、关键代码与配置
1. 惰性删除的核心函数
robj *lookupKeyRead(redisDb *db, robj *key) {
robj *val;
expireIfNeeded(db, key); // 检查键是否过期并删除
val = lookupKey(db, key);
...
return val;
}
2. 定期删除的核心函数
void activeExpireCycle(...) {
...
while (timelimit > 0 && ...) {
// 随机选取 20 个键进行过期检查
for (j = 0; j < ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP; j++) {
if (dictSize(db->expires) == 0) break;
...
// 删除过期键
if (now > when) {
...
dictDelete(db->expires, key);
server.stat_expiredkeys++;
}
}
}
}
3. 配置参数
hz
:控制serverCron
的执行频率(默认每秒 10 次)。hz 10
maxmemory-policy
:配合内存淘汰策略使用(如allkeys-lru
)。
四、实际场景中的表现
- 高并发访问场景:
- 惰性删除处理高频访问的键,定期删除清理低频访问的键。
- 冷数据场景:
- 依赖定期删除清理长期未被访问的过期键,避免内存堆积。
- 混合场景:
- 两者结合,既保证内存效率,又避免 CPU 过载。
五、常见问题与优化
1. 为什么 Redis 不使用定时删除?
- 定时删除(为每个过期键创建定时器)会导致:
- CPU资源浪费:大量定时器频繁触发,消耗 CPU。
- 内存压力大:定时器本身占用额外内存。
- Redis 选择 惰性+定期删除 的折中方案。
2. 如何优化定期删除的性能?
- 调整
hz
值:增加serverCron
的执行频率(如hz 100
),但需权衡 CPU 开销。 - 控制单次扫描范围:通过
ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP
调整每次检查的键数量。
3. 如何避免内存泄漏?
- 设置合理的过期时间:确保过期键能被定期删除及时扫描到。
- 结合内存淘汰策略:当内存不足时,通过
allkeys-lru
或volatile-ttl
等策略强制清理数据。
六、总结
策略 | 触发时机 | 优点 | 缺点 |
---|---|---|---|
惰性删除 | 客户端访问键时 | CPU 友好 | 可能导致内存泄漏 |
定期删除 | 周期性任务(100ms) | 减少内存浪费 | 占用一定 CPU 资源 |
Redis 的 惰性删除 + 定期删除 组合策略,既保证了内存的高效利用,又避免了 CPU 的过度消耗,是应对缓存场景中过期键管理的经典方案。
THE END