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时做出更贴合业务的选择。