欢迎光临
我们一直在努力

Redis内存淘汰策略与过期策略深度解析:从源码到生产配置

前言:为什么需要理解内存淘汰与过期策略?

Redis作为业界最流行的内存数据库,其核心数据全部驻留在内存中。内存是有限且昂贵的资源,当数据量增长超过可用内存时,Redis将面临两种选择:要么拒绝写入,要么按照既定规则淘汰旧数据。理解Redis的过期策略和内存淘汰机制,是保障生产环境Redis服务稳定性的基石。

很多开发者只知道给key设置过期时间,但对Redis内部的过期键清理机制却一知半解。更常见的问题是:明明设置了expire,为什么内存还在持续增长?为什么某些key过期后没有被立即删除?为什么Redis突然变慢了?这些问题的答案都隐藏在Redis的过期策略和淘汰机制中。

本文将从源码层面深度剖析Redis的过期键清理策略和8种内存淘汰策略,并给出生产环境的配置建议和监控方案。
Redis内存管理示意图

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对淘汰的影响:

  1. 淘汰延迟高DEL复杂度O(N),淘汰时Redis会短暂hang住
  2. 内存碎片:释放的大内存块易造成碎片
  3. AOF重写慢:删除命令庞大,重写扫描数据量大

解决方法:使用UNLINK替代DELredis-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压力大幅降低。

总结:核心要点

  1. 两种过期机制:被动过期(惰性删除)+ 主动过期(定期删除),CPU友好防泄漏。
  2. 8种淘汰策略:最常用allkeys-lru
  3. 近似LRU:采样+淘汰池实现,maxmemory-samples控制精度。
  4. LFU替代LRU:持续性热点用LFU,周期性热点用LRU。
  5. 避免批量过期:加入随机偏移量。
  6. 警惕大key:用UNLINK替代DEL
  7. 监控先行:内存使用率、淘汰数、命中率为核心指标。
【本站文章皆为原创,未经允许不得转载】:汤不热吧 » Redis内存淘汰策略与过期策略深度解析:从源码到生产配置
分享到: 更多 (0)