Redis缓存淘汰策略源码解析:LRU、LFU与TTL的实现分析

749 阅读3分钟

一、Redis内存淘汰全景图

Redis通过maxmemory-policy配置淘汰策略,核心策略分为三类:

  1. LRU(Least Recently Used):淘汰最久未访问的键。
  2. LFU(Least Frequently Used):淘汰访问频率最低的键。
  3. TTL(Time To Live):淘汰剩余存活时间最短的键。

设计目标:在内存不足时,以低内存开销高效计算完成键淘汰。


二、LRU:近似算法的精妙设计

1. 传统LRU的局限性
  • 理想实现:维护双向链表,移动节点至头部,淘汰尾部(时间复杂度O(1))。
  • 内存问题:每个键需存储前后指针,额外占用16字节(64位系统)。
2. Redis的近似LRU实现
  • 关键结构:每个Redis对象(redisObject)的lru字段(24位)记录访问时间戳(单位:秒级精度)。
    typedef struct redisObject {
        unsigned type:4;
        unsigned encoding:4;
        unsigned lru:LRU_BITS;  // LRU_BITS=24
        int refcount;
        void *ptr;
    } robj;
    
  • 淘汰流程
    1. 随机采样:从所有键中随机选取maxmemory-samples(默认5)个键。
    2. 淘汰候选:选择采样集中lru最小的键(即最久未访问)。

源码入口evict.c/evictionPoolPopulate()

void evictionPoolPopulate(int dbid, dict *sampledict, dict *keydict, struct evictionPoolEntry *pool) {
    // 随机采样键,计算idle时间(当前时间 - lru)
    for (j = 0; j < count; j++) {
        // 计算idle时间
        if (server.maxmemory_policy & MAXMEMORY_FLAG_LRU) {
            idle = estimateObjectIdleTime(o);
        }
        // 维护淘汰池,保留idle最大的键
    }
}

优化点

  • 内存节省:24位lru仅需3字节,精度降低但满足淘汰需求。
  • 可调精度:增大maxmemory-samples可逼近真实LRU,但增加CPU开销。

三、LFU:频率统计与衰减机制

1. LFU的核心挑战
  • 空间限制:精确统计频率需大量内存。
  • 时间衰减:旧访问记录需降低权重。
2. Redis的LFU实现
  • lru字段复用
    • 高16位:访问计数器(对数计数,范围0-255)。
    • 低8位:最后一次访问的分钟级时间戳(用于衰减)。
    // 获取计数器与衰减时间
    #define LFU_LOG_INCR 5  // 计数器增量基数
    #define LFU_DECAY_TIME 1 // 衰减周期(分钟)
    
    uint8_t LFULogIncr(uint8_t counter) {
        if (counter == 255) return 255;
        double r = (double)rand()/RAND_MAX;
        double baseval = counter - LFU_LOG_INCR;
        if (baseval < 0) baseval = 0;
        double p = 1.0/(baseval*server.lfu_log_factor+1);
        return (r < p) ? counter+1 : counter;
    }
    
  • 频率更新:每次访问时,计数器按概率增加(类似Morris计数器)。
  • 衰减机制:若当前时间与低8位时间差超过LFU_DECAY_TIME,计数器减半。

淘汰流程:选择采样集中lfu值最小的键(频率最低)。


四、TTL:时间优先的淘汰逻辑

1. 实现原理
  • expires字典:Redis维护一个哈希表,存储所有设置TTL的键及其绝对过期时间(Unix时间戳)。
  • 淘汰策略
    • volatile-ttl:从expires字典中随机采样,淘汰剩余存活时间(TTL - 当前时间)最短的键。
2. 源码片段
// 计算键的剩余存活时间
long long getExpire(redisDb *db, robj *key) {
    dictEntry *de = dictFind(db->expires,key->ptr);
    if (de) return dictGetSignedIntegerVal(de) - mstime();
    return -1;
}

// 在evictionPoolPopulate中处理TTL策略
if (server.maxmemory_policy == MAXMEMORY_VOLATILE_TTL) {
    idle = ULLONG_MAX - (long)getExpire(db,key);
}

五、策略对比与记忆技巧

策略核心指标实现要点类比记忆
LRU最近访问时间近似采样 + 时间戳比较图书馆最近借阅的书籍
LFU访问频率概率计数 + 时间衰减热门排行榜(热度会下降)
TTL剩余存活时间过期字典 + TTL排序食品保质期倒计时

配置建议

  • LRU:适用于突发访问场景,保留最近热点数据。
  • LFU:适用于长尾访问分布,保留高频访问数据。
  • TTL:适用于明确生命周期的数据(如会话信息)。