redis 如何实现 LRU

LRU 是一个缓存置换算法,在缓存有限的情况下,如果有新的数据加载至缓存,则需要考虑将不会再继续被访问的数据剔除掉,但是缓存是否会被访问是没有办法预测的,所以,LRU 是基于一个假设实现:

如果一个 key 经常被访问,那么这个 key 的空闲时间最小

这也是 LRU 实现的一个思路,它首先实现一个双向链表,当一个 key 被访问时,则将这个 key 放到双向链表的头部,当时缓存不可用时,从尾部逐个剔除

如果按照这样的假设实现,会存在一些缺陷,假设我们现在有一张数据表,执行如下 SQL 语句:

SELECT * FROM table_name;

上面这条 SQL 的作用是将数据表中的所有数据读取出来,我们再将该数据表中的所有数据读取出后就不再继续使用,那么对于 LRU 的双向链表在头部会有大量数据占用,导致热点数据被逐出缓存以致于会出现大量磁盘 I/O

MySQL Innodb 的 buufer pool 实现了一个改进版的 LRU,它将 LRU 的双向链表分为两部分,一个是 newlist 另一个是 oldlist,newlist 主要是用于存放头部热点数据,oldlist 用于存放非热点数据,当首次加载一个 page 时,会将数据放到 oldlist 的头部,再次访问的时候会移动到 newlist

而对于 redis ,redis 整体是一个大的 dict,如果要想实现双向链表,需要给每一个 key 新增两个指针,占用 16 个字节大小,并且需要一个额外的 list 结构存储双向链表的头尾节点信息,这样会占用一定的内存空间,导致 redis 性能下降,所以,redis 并没有实现双向链表

redis 整体是一个大的 dict,每一个 value 被保存为一个 redisobj 结构,每一个 redisobj 结构都包含有一个 lru 字段,该字段存储的是一个时间戳,当根据 key 获取值的时候,会调用 lookup 函数,如果开启了 LRU 模式,则该函数会将 lru 的替换成当前秒级的时间戳,然后 redis 再使用随机采样法,从 dict 中筛选出 5 个key(注意:这里的 5 个 key 是可以修改的,由 maxmemory-smples 控制),比较 lru 值的大小,淘汰掉最小的那个

在 redis 3.0 以后对该算法进行了一个升级,新的算法维护了一个候选池(pool),首次筛选出来的 key 会被全部放入到候选池中,在后续的筛选过程中只有 lru 小于候选池中最小的 lru 才能被放入到候选池,直至候选池放满,当候选池满了的时候,如果有新的数据继续放入,则需要将候选池中 lru 字段最大值取出

然后在淘汰的时候,只需要将候选池中 lru 字段值最小的淘汰掉即可

你可能感兴趣的:(redis 如何实现 LRU)