📚 什么是过期键删除策略?
在Redis中,我们可以为键设置过期时间(TTL, Time To Live)。当键的过期时间到达后,Redis需要将其从数据库中删除。但是,Redis并不会在键过期的瞬间立即删除它,而是采用多种策略结合的方式来删除过期键。
这是因为:
- 性能考虑:如果为每个过期键都创建一个定时器,会消耗大量CPU资源
- 内存考虑:如果让过期键一直存在,会浪费内存空间
- 折中方案:Redis采用多种策略结合,在CPU时间和内存空间之间取得平衡
⚙️ Redis的三种过期键删除策略
1️⃣ 定时删除(Active Expire / Immediate Deletion)
定义:在设置键的过期时间的同时,创建一个定时器(timer),让定时器在键的过期时间来临时,立即执行对键的删除操作。
✅ 优点
- 内存友好:过期键会在过期时立即被删除,释放内存
- 及时性高:保证键在过期时立即被删除
❌ 缺点
- CPU压力大:需要为每个设置了过期时间的键创建定时器
- 性能影响:当过期键很多时,会占用大量CPU时间
- 实现复杂:需要维护大量的定时器
Redis的实际应用:Redis实际上不采用这种策略作为主要的过期删除策略,因为会给CPU带来太大压力。
2️⃣ 惰性删除(Lazy Expire / Passive Deletion)
定义:放任键过期不管,但是每次从键空间中获取键时,都检查取得的键是否过期,如果过期的话,就删除该键;如果没有过期,就返回该键。
✅ 优点
- CPU友好:只有在取出键时才会进行过期检查,不浪费CPU时间
- 实现简单:不需要维护定时器
- 对CPU时间最友好
❌ 缺点
- 内存不友好:过期键可能长时间存在,造成内存浪费
- 可能导致内存泄漏:如果过期键一直不被访问,会一直占用内存
Redis的实现:Redis的惰性删除是通过db.c/expireIfNeeded函数实现的。所有Redis命令在读取键之前都会调用这个函数检查键是否过期。
int expireIfNeeded(redisDb *db, robj *key) {
if (!keyIsExpired(db, key)) return 0; // 键未过期
if (server.masterhost != NULL) return 1; // 从节点不删除,等待主节点同步
// 发送过期通知
notifyKeyspaceEvent(NOTIFY_EXPIRED, "expired", key, db->id);
// 从数据库中删除键
return dbDelete(db, key);
}
3️⃣ 定期删除(Scheduled Expire / Periodic Deletion)
定义:每隔一段时间,程序就对数据库进行一次检查,删除里面的过期键。至于要删除多少过期键,以及要检查多少个数据库,则由算法决定。
✅ 优点
- 折中方案:通过限制删除操作执行的时长和频率来减少CPU时间的影响
- 定期清理:能够定期释放过期键占用的内存
- 避免内存泄漏:定期删除过期键,防止内存浪费
❌ 缺点
- 难以确定执行时长:如果删除操作执行得太频繁,会占用大量CPU时间
- 难以确定执行频率:如果删除操作执行得太少,会造成内存浪费
Redis的实现:Redis的定期删除是通过expire.c/activeExpireCycle函数实现的。这个函数会在Redis的周期性执行任务中被调用。
📊 定期删除的工作流程:
Step 1: 初始化
函数每次运行时,都从一定的数据库中取出一定数量的键进行检查,并删除其中的过期键。
Step 2: 全局变量跟踪
有一个全局变量 current_db 会记录检查进度,直到所有数据库都被检查一遍后,重置为0。
Step 3: 时间限制
每次运行都有时间限制(REDIS_EXPIRELOOKUPS_TIME_LIMIT),防止占用过多CPU时间。
Step 4: 采样删除
从每个数据库的expires字典(存储过期时间)中随机取出一定数量的键进行检查,删除过期的键。
void activeExpireCycle(int type) {
for (int j = 0; j < dbs_per_call; j++) {
// 从expires字典中随机取key
for (int i = 0; i < ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP; i++) {
dictEntry *de = dictGetRandomKey(db->expires);
if (keyIsExpired(db, key)) {
dbDelete(db, key); // 删除过期键
}
}
}
}
🔧 Redis实际使用的策略:惰性删除 + 定期删除
Redis实际上采用了惰性删除和定期删除两种策略的结合:
💡 为什么采用这两种策略的结合?
- 惰性删除保证了过期键在下次被访问时一定会被删除,避免了内存泄漏
- 定期删除则定期清理过期键,减少了惰性删除可能带来的内存浪费
- 两种策略互补,在CPU时间和内存空间之间取得了良好的平衡
📝 工作流程总结:
- 客户端读取键时:Redis会先调用
expireIfNeeded函数检查键是否过期,如果过期则删除并返回nil - 定期任务执行时:Redis会调用
activeExpireCycle函数定期清理过期键 - 内存淘汰时:当内存达到上限时,Redis会根据配置的淘汰策略(maxmemory-policy)来删除键
📊 三种策略对比
| 策略 | 优点 | 缺点 | Redis是否采用 |
|---|---|---|---|
| 定时删除 | 内存友好,及时删除 | CPU压力大,实现复杂 | ❌ 不采用 |
| 惰性删除 | CPU友好,实现简单 | 内存不友好,可能内存泄漏 | ✅ 采用 |
| 定期删除 | 折中方案,平衡CPU和内存 | 难以确定时长和频率 | ✅ 采用 |
⚙️ Redis相关配置参数
hz 参数
作用:控制Redis的定时任务执行频率,间接影响定期删除的执行频率。
默认值:10(表示每秒执行10次)
取值范围:1-500
# 提高hz值会增加定期删除的频率,但也会消耗更多CPU
hz 10
# 对于过期键较多的场景,可以适当提高hz值
# 但不建议超过100,否则会影响性能
hz 50
maxmemory-policy 参数
作用:当内存达到上限时,Redis的淘汰策略。虽然不是直接的过期键删除策略,但会影响键的删除行为。
可选值:
noeviction: 不淘汰,写操作报错allkeys-lru: 从所有键中淘汰最久未使用的volatile-lru: 从设置了过期时间的键中淘汰最久未使用的allkeys-random: 从所有键中随机淘汰volatile-random: 从设置了过期时间的键中随机淘汰volatile-ttl: 淘汰剩余生存时间最短的键allkeys-lfu: 从所有键中淘汰使用频率最低的(Redis 4.0+)volatile-lfu: 从设置了过期时间的键中淘汰使用频率最低的(Redis 4.0+)
✅ 最佳实践建议
1. 合理设置过期时间
避免大量键在同一时刻过期,造成"过期风暴"。可以采用随机化过期时间的方法:
import random
expire_time = 3600 + random.randint(0, 300) # 1小时 ± 5分钟
redis_client.setex('key', expire_time, 'value')
2. 监控过期键数量
使用INFO stats命令查看expired_keys指标,监控过期键的删除情况。
127.0.0.1:6379> INFO stats
# 关注以下指标:
# expired_keys: 过期键被删除的总数
# evicted_keys: 因内存达到上限被淘汰的键总数
3. 适当调整hz参数
如果你的应用中过期键非常多,可以适当提高hz参数的值,让定期删除更频繁地执行。
4. 使用惰性删除的注意事项
惰性删除意味着过期键可能不会被立即删除。如果你需要精确控制键的生命周期,应该在应用层做好处理,而不是完全依赖Redis的过期机制。
❓ 常见问题解答
Q1: Redis如何知道哪些键过期了?
答:Redis使用一个叫做expires的字典(哈希表)来存储所有设置了过期时间的键及其过期时间。当检查一个键是否过期时,Redis会在这个字典中查找该键的过期时间,然后与当前Unix时间戳比较。
Q2: 主从复制环境下,过期键如何删除?
答:在Redis的主从复制模式下:
- 主节点:负责删除过期键,并生成DEL命令同步到从节点
- 从节点:不会主动删除过期键,而是等待主节点的DEL命令
- 一致性保证:即使从节点读取到过期键,也不会删除,而是返回给客户端(Redis 3.2之后,从节点也会检查键是否过期,如果过期则返回空)
Q3: 内存达到上限时,Redis会如何处理过期键?
答:当内存达到maxmemory限制时,Redis会根据maxmemory-policy配置的策略来删除键。如果配置的是volatile-*策略,Redis会优先删除设置了过期时间的键。
Q4: 如何手动删除过期键?
答:虽然不能手动触发定期删除,但可以通过以下方式:
- 使用
SCAN命令遍历所有键,逐个访问触发惰性删除 - 重启Redis(不推荐,会中断服务)
- 等待定期删除任务自动清理
🎯 总结
核心要点:
- Redis采用惰性删除和定期删除相结合的策略来处理过期键
- 惰性删除在访问键时检查并删除过期键,CPU友好但可能内存浪费
- 定期删除定期抽样检查并删除过期键,平衡CPU和内存使用
- 两种策略互补,既避免了CPU过度占用,又防止了内存泄漏
- 在实际应用中,应根据业务场景合理设置过期时间,并监控过期键的删除情况
💡 记住:Redis的过期键删除是一个精心设计的平衡艺术,在CPU时间和内存空间之间取得了最佳平衡!