前言:为什么需要理解内存淘汰与过期策略?
Redis作为业界最流行的内存数据库,其核心数据全部驻留在内存中。内存是有限且昂贵的资源,当数据量增长超过可用内存时,Redis将面临两种选择:要么拒绝写入,要么按照既定规则淘汰旧数据。理解Redis的过期策略和内存淘汰机制,是保障生产环境Redis服务稳定性的基石。
很多开发者只知道给key设置过期时间,但对Redis内部的过期键清理机制却一知半解。更常见的问题是:明明设置了expire,为什么内存还在持续增长?为什么某些key过期后没有被立即删除?为什么Redis突然变慢了?这些问题的答案都隐藏在Redis的过期策略和淘汰机制中。
本文将从源码层面深度剖析Redis的过期键清理策略和8种内存淘汰策略,并给出生产环境的配置建议和监控方案。

Redis过期策略的两层机制
Redis对设置了过期时间的key采用被动过期和主动过期两种方式配合清理,确保过期键既不会被返回给客户端,也不会长期占用内存。
被动过期(惰性删除)
当客户端访问一个key时,Redis会在
1 | lookupKeyRead() |
函数中检查该key是否已过期。如果已过期,则将其删除并返回nil。这个机制在
1 | expireIfNeeded() |
函数中实现:
1
2
3
4
5
6
7
8
9
10
11
12 // Redis源码:db.c - expireIfNeeded()
int expireIfNeeded(redisDb *db, robj *key) {
mstime_t when = getExpire(db, key);
if (when < 0) return 0;
if (server.loading) return 0;
mstime_t now = mstime();
if (now > when) {
deleteExpiredKeyAndPropagate(db, key);
return 1;
}
return 0;
}
被动过期的好处是CPU友好——只删除被访问到的过期key。但缺点是过期key若再不被访问将永远占用内存,造成内存泄漏,因此需要主动过期机制来兜底。
主动过期(定期删除)
Redis的主动过期由
1 | serverCron() |
定时任务驱动,每100ms执行一次
1 | activeExpireCycle() |
函数。该函数从每个数据库的过期字典中随机采样一批key,删除已过期的key:
1
2
3
4
5
6
7
8
9
10
11
12
13 // Redis源码:expire.c - activeExpireCycle()
void activeExpireCycle(int type) {
unsigned long num = 20, expired = 0;
while (num--) {
dictEntry *de = dictGetRandomKey(db->expires);
if (expireIfNeeded(db, de)) expired++;
}
if (expired > 20/4) {
do {
// 再次采样20个key
} while (expired > 20/4 && loops++ < 16);
}
}
| 参数 | 默认值 | 说明 |
|---|---|---|
| ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP | 20 | 每次循环检查的key数量 |
| ACTIVE_EXPIRE_CYCLE_FAST_DURATION | 1000μs | 快速模式最大耗时 |
| ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC | 25 | 慢速模式最多占用CPU时间的百分比 |
Redis的8种内存淘汰策略
当Redis使用的内存达到
1 | maxmemory |
限制时,触发内存淘汰机制。Redis 6.x+ 提供以下8种策略:
| 策略 | 说明 | 适用场景 | ||
|---|---|---|---|---|
|
不淘汰,写入报错(OOM) | 缓存不允许丢失 | ||
|
所有key按LRU淘汰 | 通用缓存(最常用) | ||
|
所有key按LFU淘汰 | 访问频率差异大 | ||
|
有TTL的key按LRU淘汰 | 混合缓存与持久数据 | ||
|
有TTL的key按LFU淘汰 | 同上,关注访问频率 | ||
|
所有key随机淘汰 | 数据访问无规律 | ||
|
有TTL的key随机淘汰 | 激进淘汰保持久key | ||
|
按TTL时间淘汰 | 尽快淘汰即将过期的key |
noeviction:永不淘汰
默认策略。内存达上限时写入操作返回OOM错误,读取不受影响。适用于不允许丢失数据的场景,如用Redis做数据库。需配合精确的容量规划。
LRU策略:最近最少使用
LRU是最常用的策略。Redis实现的是近似LRU,采用采样方式避免全量LRU链表的高成本:
1
2
3
4
5
6
7
8
9 // evict.c - evictionPoolPopulate()
void evictionPoolPopulate(dict *sampledict, ...) {
unsigned long long idle = estimateObjectIdleTime(o);
for (j = 0; j < EVICTION_POOL_SIZE; j++) {
if (pool[j].idle < de.idle) {
// 插入排序,保持idle time降序
}
}
}
关键配置:
1 | maxmemory-samples |
(默认5),建议生产设为10。采样数5时近似LRU命中率已超理论LRU的99%。
LFU策略:最不经常使用
Redis 4.0引入的LFU比LRU更能抵抗突发流量冲击。LFU淘汰访问频率最低的key,而非最久未访问的key。实现上复用了LRU的24位lru字段:
- 高16位:上次访问时间(分钟级)
- 低8位:对数计数器(counter),存储访问频率
1
2
3
4
5
6
7
8 #define LFU_INIT_VAL 5
uint8_t LFULogIncr(uint8_t counter) {
if (counter == 255) return 255;
double r = (double)rand() / RAND_MAX;
double p = 1.0 / (counter * LFU_LOG_FACTOR + 1);
if (r < p) counter++;
return counter;
}
LFU还引入counter衰减机制,
1 | lfu-decay-time |
参数控制衰减速度。
经验之谈:周期性热点(如每天早高峰)LFU可能不如LRU,因LFU保留历史高频记录。持续性热点场景LFU表现更好。
生产环境配置最佳实践
选择正确的淘汰策略
1
2
3
4
5
6 # redis.conf 推荐配置
maxmemory 8gb
maxmemory-policy allkeys-lru
maxmemory-samples 10
lfu-decay-time 1
lfu-log-factor 10
合理设置过期时间
避免大量key同时过期导致CPU突增,加入随机偏移量:
1
2
3
4
5
6 // Java示例:随机化过期时间
public static long randomExpire(int baseSeconds) {
int jitter = (int)(baseSeconds * 0.2 * Math.random());
return baseSeconds + jitter;
}
jedis.setex("user:1001", randomExpire(3600), userJson);
监控与告警
| 指标 | 命令 | 告警阈值 | ||
|---|---|---|---|---|
| 内存使用率 |
|
> 85% | ||
| 淘汰数量 |
|
突增立即告警 | ||
| 过期key数量 |
|
突增检查批量过期 | ||
| 键命中率 |
|
< 85% | ||
| 延迟 |
命令 |
> 100ms |
1
2
3
4
5
6
7
8
9
10
11 #!/bin/bash
THRESHOLD=85
REDIS_CLI="redis-cli -h localhost -p 6379"
USAGE=$($REDIS_CLI INFO memory | grep "used_memory" | head -1 | awk -F: '{print $2}')
MAXMEM=$($REDIS_CLI CONFIG GET maxmemory | tail -1)
RATIO=$((USAGE * 100 / MAXMEM))
echo "内存使用率: ${RATIO}%"
if [ $RATIO -gt $THRESHOLD ]; then
EVICTED=$($REDIS_CLI INFO stats | grep "evicted_keys" | awk -F: '{print $2}')
echo "WARNING: 内存使用率超过${THRESHOLD}%! 已淘汰: ${EVICTED}"
fi
大key对淘汰机制的影响
大key是Redis线上最头疼的问题,通常指:
- String值超过10KB
- 集合类型元素超过5000个
- 整体value超过1MB
大key对淘汰的影响:
- 淘汰延迟高:
1DEL
复杂度O(N),淘汰时Redis会短暂hang住
- 内存碎片:释放的大内存块易造成碎片
- AOF重写慢:删除命令庞大,重写扫描数据量大
解决方法:使用
1 | UNLINK |
替代
1 | DEL |
、
1 | redis-cli --bigkeys |
扫描、业务层面拆分大key。
Redis 7.0+ 的新变化
自动内存碎片整理增强
Redis 7.0改进
1 | ACTIVE DEFRAGMENTATION |
,可自动检测并整理碎片。新增
1 | jemalloc-bg-thread |
配置,利用后台线程回收内存。
AOF文件在淘汰中的优化
Redis 7.0引入multi-part AOF机制,将AOF拆分为base和incr文件,淘汰场景下AOF重写IO压力大幅降低。
总结:核心要点
- 两种过期机制:被动过期(惰性删除)+ 主动过期(定期删除),CPU友好防泄漏。
- 8种淘汰策略:最常用
1allkeys-lru
。
- 近似LRU:采样+淘汰池实现,
1maxmemory-samples
控制精度。
- LFU替代LRU:持续性热点用LFU,周期性热点用LRU。
- 避免批量过期:加入随机偏移量。
- 警惕大key:用
1UNLINK
替代
1DEL。
- 监控先行:内存使用率、淘汰数、命中率为核心指标。
汤不热吧