Redis中LRU与LFU的底层实现:字节级的精巧设计

发布于:2025-08-19 ⋅ 阅读:(17) ⋅ 点赞:(0)

Redis对LRU和LFU的实现,藏在源码的细节里。这些设计不仅要保证缓存淘汰的合理性,更要兼顾内存占用与操作效率,每一个字节的使用都经过精心考量。

一、LRU:时间戳的存储与采样淘汰的完整逻辑

1. 键对象中的时间戳存储

在Redis中,每个键对应的redisObject结构体里,有一个lru字段(32位无符号整数)。对于LRU策略,这个字段被用来存储最后一次访问的时间戳(精确到秒)。但这里的时间戳并非系统当前时间的直接记录,而是与Redis的"时钟"同步的相对值——Redis会维护一个全局变量server.lruclock,每秒钟更新一次,lru字段记录的正是键最后一次被访问时server.lruclock的值。

这种设计的巧妙之处在于:当计算键的空闲时间时,只需用当前server.lruclock减去lru字段的值(若当前时钟小于lru值,说明时钟溢出,需用(2^32 - lru) + server.lruclock计算),无需频繁调用系统时间函数,减少了性能开销。

2. 淘汰流程的完整步骤

当内存达到maxmemory阈值时,LRU的淘汰流程按以下步骤执行:

  • 步骤1:构建候选集
    从所有符合条件的键(如volatile-lru只考虑带过期时间的键)中,随机选取maxmemory-samples个键(默认5个),放入候选集。这个随机采样并非完全随机,Redis会优先从"可能需要淘汰"的键集中选取,减少对活跃键的无效采样。
  • 步骤2:筛选淘汰键
    遍历候选集,计算每个键的空闲时间(当前lruclock - 键的lru值),找出空闲时间最长的键作为淘汰目标。
  • 步骤3:循环淘汰
    删除选中的键,释放内存后检查是否满足内存要求。若仍不满足,重复步骤1-2,直到内存低于阈值或所有符合条件的键被淘汰完毕。

3. 近似LRU的精度控制

maxmemory-samples参数直接影响LRU的精度:值越小,采样越快,但可能漏掉真正该淘汰的键;值越大(如设为10),候选集越接近全量键,淘汰精度越高,但采样耗时增加。Redis默认值5是在大量测试后得出的平衡值,既能保证多数场景的命中率,又不会带来明显性能损耗。

二、LFU:8位计数器的概率增长与衰减机制

LFU在Redis 4.0中引入,其lru字段被重新拆分为两部分:高8位用于存储访问频率计数器(logc),低16位存储最后一次访问的时间戳(ldt)。这种拆分既节省了内存,又实现了频率与时间的双重跟踪。

1. 频率计数器(logc)的增长逻辑

计数器并非简单的访问次数累加,而是采用概率性增长

  • 当键被访问时,Redis会生成一个随机数(0-1之间的浮点数);
  • 计算当前计数器值的"阈值":p = 1 / (counter + 1)
  • 若随机数小于p,则计数器加1(最高不超过255,因只有8位)。

例如:当计数器为0时,p=1/1=1,每次访问必加1;计数器为1时,p=1/2,有50%概率加1;计数器为10时,p=1/11≈0.09,只有9%概率加1。这种设计避免了高频访问键的计数器迅速溢出,同时让计数器值更能反映"相对频率"。

2. 频率的衰减机制

为防止"过去高频但现在低频"的键长期占用内存,LFU设计了时间衰减

  • 计算键的空闲时间:idle = 当前时间 - 低16位记录的ldt
  • 根据lfu_decay_time配置(默认1分钟),计算衰减步数:decay = idle / lfu_decay_time
  • decay > 0,则计数器减去decay(最低减至0),并更新ldt为当前时间。

例如:若lfu_decay_time=10分钟,一个键空闲了30分钟,decay=3,计数器会减3。这确保了长期未被访问的键,即使历史频率高,也会逐渐失去"保护"。

3. 淘汰的双重判断标准

LFU淘汰时,候选集的筛选逻辑比LRU更复杂:

  • 首先比较计数器值(logc),值越小越优先被淘汰;
  • 若计数器值相同,则比较低16位的时间戳(ldt),时间越旧(空闲越久)越优先被淘汰。

这种"频率为主,时间为辅"的规则,完美解决了"同频键如何取舍"的问题。

三、两种策略的内存开销与性能对比

从底层实现看,LRU和LFU的内存开销完全相同(均使用redisObject的32位lru字段),但操作逻辑的复杂度不同:

  • LRU:只需更新时间戳和简单的空闲时间计算,操作成本极低,适合对性能敏感、数据访问具有短期局部性的场景(如会话缓存)。
  • LFU:需维护计数器的增长与衰减,每次访问和淘汰时的计算量更大,但能更精准地保留长期高频数据,适合数据访问模式稳定的场景(如API接口缓存)。

Redis的设计哲学在此体现得淋漓尽致:不追求理论上的"绝对完美",而是通过精巧的字节级优化,让算法在实际场景中既能高效运行,又能满足业务对缓存命中率的需求。理解这些底层细节,才能在配置Redis时做出更贴合业务的选择。


网站公告

今日签到

点亮在社区的每一天
去签到