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

Redis过期策略的两层机制
Redis对设置了过期时间的key采用被动过期和主动过期两种方式配合清理,确保过期键既不会被返回给客户端,也不会长期占用内存。
被动过期(惰性删除)
当客户端访问一个key时,Redis会在lookupKeyRead()函数中检查该key是否已过期。如果已过期,则将其删除并返回nil。这个机制在expireIfNeeded()函数中实现:
// 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的主动过期由serverCron()定时任务驱动,每100ms执行一次activeExpireCycle()函数。该函数从每个数据库的过期字典中随机采样一批key,删除已过期的key:
// 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使用的内存达到maxmemory限制时,触发内存淘汰机制。Redis 6.x+ 提供以下8种策略:
| 策略 | 说明 | 适用场景 |
|---|---|---|
noeviction |
不淘汰,写入报错(OOM) | 缓存不允许丢失 |
allkeys-lru |
所有key按LRU淘汰 | 通用缓存(最常用) |
allkeys-lfu |
所有key按LFU淘汰 | 访问频率差异大 |
volatile-lru |
有TTL的key按LRU淘汰 | 混合缓存与持久数据 |
volatile-lfu |
有TTL的key按LFU淘汰 | 同上,关注访问频率 |
allkeys-random |
所有key随机淘汰 | 数据访问无规律 |
volatile-random |
有TTL的key随机淘汰 | 激进淘汰保持久key |
volatile-ttl |
按TTL时间淘汰 | 尽快淘汰即将过期的key |
noeviction:永不淘汰
默认策略。内存达上限时写入操作返回OOM错误,读取不受影响。适用于不允许丢失数据的场景,如用Redis做数据库。需配合精确的容量规划。
LRU策略:最近最少使用
LRU是最常用的策略。Redis实现的是近似LRU,采用采样方式避免全量LRU链表的高成本:
// 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降序
}
}
}
关键配置:maxmemory-samples(默认5),建议生产设为10。采样数5时近似LRU命中率已超理论LRU的99%。
LFU策略:最不经常使用
Redis 4.0引入的LFU比LRU更能抵抗突发流量冲击。LFU淘汰访问频率最低的key,而非最久未访问的key。实现上复用了LRU的24位lru字段:
- 高16位:上次访问时间(分钟级)
- 低8位:对数计数器(counter),存储访问频率
#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衰减机制,lfu-decay-time参数控制衰减速度。
经验之谈:周期性热点(如每天早高峰)LFU可能不如LRU,因LFU保留历史高频记录。持续性热点场景LFU表现更好。
生产环境配置最佳实践
选择正确的淘汰策略
# redis.conf 推荐配置
maxmemory 8gb
maxmemory-policy allkeys-lru
maxmemory-samples 10
lfu-decay-time 1
lfu-log-factor 10
合理设置过期时间
避免大量key同时过期导致CPU突增,加入随机偏移量:
// 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);
监控与告警
| 指标 | 命令 | 告警阈值 |
|---|---|---|
| 内存使用率 | INFO memory → used_memory/maxmemory |
> 85% |
| 淘汰数量 | INFO stats → evicted_keys |
突增立即告警 |
| 过期key数量 | INFO stats → expired_keys |
突增检查批量过期 |
| 键命中率 | keyspace_hits/keyspace_misses |
< 85% |
| 延迟 | latency命令 |
> 100ms |
#!/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对淘汰的影响:
- 淘汰延迟高:
DEL复杂度O(N),淘汰时Redis会短暂hang住 - 内存碎片:释放的大内存块易造成碎片
- AOF重写慢:删除命令庞大,重写扫描数据量大
解决方法:使用UNLINK替代DEL、redis-cli --bigkeys扫描、业务层面拆分大key。
Redis 7.0+ 的新变化
自动内存碎片整理增强
Redis 7.0改进ACTIVE DEFRAGMENTATION,可自动检测并整理碎片。新增jemalloc-bg-thread配置,利用后台线程回收内存。
AOF文件在淘汰中的优化
Redis 7.0引入multi-part AOF机制,将AOF拆分为base和incr文件,淘汰场景下AOF重写IO压力大幅降低。
总结:核心要点
- 两种过期机制:被动过期(惰性删除)+ 主动过期(定期删除),CPU友好防泄漏。
- 8种淘汰策略:最常用
allkeys-lru。 - 近似LRU:采样+淘汰池实现,
maxmemory-samples控制精度。 - LFU替代LRU:持续性热点用LFU,周期性热点用LRU。
- 避免批量过期:加入随机偏移量。
- 警惕大key:用
UNLINK替代DEL。 - 监控先行:内存使用率、淘汰数、命中率为核心指标。
汤不热吧