了解LRU
工业级实现,以MySQL
、Redis
为例。
LRU基本实现及优化 中,我介绍了LRU
算法的基本逻辑并给出了实现,本文我们来了解下工业级应用是怎么设计、实现LRU
的。
低频访问的数据,如果继续留存在缓存中的话,会浪费缓存空间、内存。这就是缓存污染。
LRU
是一种缓存替换、淘汰策略,这种策略核心目的就是尽快淘汰缓存的污染项。
MySQL
版本:5.7
MySQL
的Buffer Pool
是一块缓冲区域,用内存存储了表、索引信息,加速频繁操作的数据读取。
Buffer Pool结构
借用官方文档的图:
Buffer Pool
整体依然是一个链表结构,每一个节点是一个page
,一个page
中可容纳多行数据(对CPU
缓存行更友好)。
相比我们实现的双向链表,MySQL Buffer Pool
链表分成了两大区域:
- New Sublist
New
区数据由Old
区被频繁访问的数据晋升而来,表示的是读取更多更需要缓存的数据
- Old Sublist
- 新缓存先进入
Old
区,如无频繁读取则快速淘汰
- 新缓存先进入
设计剖析
New
区Old
区,这是典型的分代设计。默认比例:New
5Old
3。
新老代区分,是为了提高缓存的命中率,淘汰不太需要的数据。
比如有些全量查询会将大量数据丢入Buffer Pool
内存,而后续这些数据的读取很少,如果仅用我们之前的实现,这些数据就需要更久的时间被淘汰,也就是缓存的污染程度变高了。
分代之后,读取更多的数据很快晋升到New
区,而Old
区比例更小,读取不频繁的数据可以更快被淘汰。
Monitoring the Buffer Pool Using the InnoDB Standard Monitor 记录了InnoDB Standard Monitor
下Buffer Pool
相关的监控指标项,其中有的指标与LRU
有关。
小结
InnoDB
中的Buffer Pool
LRU
设计是一种变体,加入了分代的设计,区分了高频、低频访问的数据,能更快地淘汰低频访问的数据,提高了内存的利用率、缓存的命中率。
Redis
版本:4.0
Redis中提供了多种淘汰策略:
- noeviction
- volatile-random
- volatile-ttl
- volatile-lru
- volatile-lfu
- allkeys-lru
- allkeys-random
- allkeys-lfu
其中volatile
表示淘汰针对的是设置了过期时间的数据,allkeys
针对所有数据。
lru
后缀则是本文关注的算法相关项。
Redis
通过maxmemory
配置服务端的最大内存容量,实际数据超出这个值之后,就开始执行淘汰策略。而volatile
类型的key
则只要达到过期时间就开始淘汰。
Redis
的LRU
设计也是另一种变体,是一种近似的算法:Approximated LRU algorithm。
每次淘汰时,对maxmemory-samples 5
【配置项】个数据进行采样,选其中最应该淘汰的数据剔除内存。
效果:
设计剖析
Redis
源码相对少很多,我们从源码的角度来看看。
redisObject 全局对象
Redis
全局对象redisObject
定义了一个lru
时间戳字段:
|
|
db.c 定义db级别的c api
lookupKey 函数负责从全局哈希表中找到对应的kv
。
|
|
可以看到16行会更新lru
时间戳。
server.c 定义服务端全局行为
server.c 中的serverCron负责定时任务的执行,注释上可以看到:
- 以
server.hz
配置项为周期运行 - 通过定时的方式完成异步任务
- 其中就包括本文涉及的
key
淘汰任务的触发,这类任务是一种惰性的查找式设计
- 其中就包括本文涉及的
processCommand
处理请求,其中会检查内存是否达到maxmemory
,调用了freeMemoryIfNeeded
,以此来触发缓存淘汰流程。
服务端启动时,调用了evictionPoolAlloc
,这里会初始化lru
淘汰使用的一个池,内部是evictionPoolEntry
结构,主要记录了keyname
、idle
字段。
freeMemoryIfNeeded
中调用 evictionPoolPopulate
来更新待淘汰的键值数据。
之后直接调用删除内存的实现。
采样筛选的逻辑
|
|
小结
Redis
中实现的LRU
变体流程:
processCommand
处理请求,检查内存是否达到maxmemory
,定时任务中也会有触发入口;freeMemoryIfNeeded
中检查、计算待释放的内存;evictionPoolPopulate
更新淘汰池;- 采样,选择采样样本中最应该淘汰的
bestKey
; - 执行淘汰;
核心是近似获取待删除的数据key
。
近似的目的是为了节省内存,保证精确的链表结构需要维护在内存中。同时定时定量拉取数据保证了淘汰操作只占用恒定的内存,有利于Redis
服务端的稳定运行。
总结
MySQL
中主要通过分代来更快淘汰污染的缓存。而Redis
主要通过定量拉取、近似采样的方式节省服务端内存,也保证了服务稳定运行。
可以看到工业级实现会有更多的考虑:
- 判断服务端真实使用场景;
- 节省内存;
- 保证服务端稳定;