LRU 算法
LRU算法原理及实现
前言
什么是LRU算法?
LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。
那么该数据结构就是当存储队列到达上限时,清除的是最久未被访问的节点,该节点一般认为是最可能无用的节点,保留下来的是最近都有使用过的节点,因此可以实现对"有用"数据的最大程度保留。
LRU算法应用场景?
LRU算法有许多的应用场景。
- Redis中使用LRU来进行淘汰
- 操作系统底层的内存管理,比如说页面置换算法中的LRU算法
- 业务处理,比如说做一个用户最近10个浏览记录,那么就可以使用LRU算法来维护一个大小为10的LRU队列
原理
LRU算法需要实现如下特性
- 实现get/put方法(都为O(1)的时间复杂度)
- 每次get时需要将访问的节点提前至队首
- 每次put需要判断队列是否已满,满了则将最后的节点删除,并且将该节点放至队首,不满则直接放队首
基于上述特性需要实现如下数据结构
- 首先需要实现队列,如果使用单向链表,当我们需要使用删除操作时,需要获得前置节点的指针,单向链表则不能做到直接获取。因此使用双向链表。
- 又我们需要get方法达到O(1)的时间复杂度,因此需要一个Hashmap,可以根据key定位到我们双向链表的Node节点。
- 由于我们HashMap中有key,所以我们可不可以Node中只存value,其实是不可以的,在队满时需要通过删除链表最后节点的 key 来反向找到 hash 表中应该覆盖的 key。
因此我们实现了如下数据结构
缓存淘汰过程如下
实现优化考虑
上述的LRU容器还是一个根本不能投入生产使用的玩具级实现,可以在进一步进行优化。
值的类型
上述的实现我们都是默认value是int,而且是正整数的int,然而生产中,不应该使用固定的value值。Java中应该使用泛型,GoLang中可以使用interface。
最大容量
上述我们的容器最大容量的单位是键值对的个数,这是不太合理的,因为实际中我们应该限制的是缓存占用大小,因此可以将最大限制改成byte为单位,而且需要对淘汰算法进行优化,这时候我们可能超出容量后,需要淘汰的不止是一个缓存,可以是多个,直到当前已用内存小于最大内存。
并发安全
上述我们写的只能在单线程下使用,没有考虑到并发问题,那么其实只需要对每次链表和队列的写查进行相应的加锁即可。Java可以使用synchronized关键字也可以使用ReentrantLock/ReentrantReadWriteLock来对其加锁。GoLang可以使用标准库的sync.Mutex来加锁。
其他
我们这次写的是每次都更新到首位的LRU,称为lru-1,也有lru-k的方式,这个需要根据情况进行优化。
LRU-K(Least Recently Used K)是一种基于LRU算法的变种,它在维护缓存中的数据访问顺序时,不仅考虑最近一次访问,还考虑了最近K次的访问情况。
具体来说,LRU-K算法会在缓存中记录每个数据最近K次被访问的位置,而不仅仅是最近一次。当需要淘汰数据时,LRU-K会选择最久未被访问的那些数据,也就是在过去K次访问中没有被访问的数据。这样做的目的是更好地适应特定访问模式,对于某些场景可以提供更好的性能。
LRU-K中的K可以根据实际应用情况来调整。较小的K值会更关注短期内的访问模式,而较大的K值则更加平滑地适应长期访问模式。选择合适的K值需要结合实际数据访问模式和性能需求进行权衡。
总之,LRU-K是一种对传统的LRU算法的扩展和改进,通过考虑多次访问的历史记录,可以更精确地判断哪些数据是最久未被使用的,从而在某些情况下提供更好的缓存性能。