前言
互联网项目为了提升系统吞吐量,同时为了减少数据库的负载压力,通常会引入缓存,比如缓存用户的姓名、头像、等级等等之类的信息。在绝大部分的情况下(百万级用户),这么做都是没问题的,实现简单,同时也高效。但是当我们的系统用户量达到千万级以后,这么做就会遇到如下问题:
- 占用内存过大,容易挤爆内存,同时内存又是非常稀缺有昂贵的资源
- 针对用户信息进行全量缓存,并不一定所有的缓存都被利用,如果你们app的用户量达到百万级,同时日活达到20W,你们的app就已经很成功了。但是会浪费绝大部分的资源,如果用户量达到千万级,缓存命中率可能会更低
如何解决上面这两点问题,针对1来说,为了防止内存被挤满,我们可以设置一个阀值。当缓存到某个程度容量,就清除一半的缓存;但是对于2来说,如果我们清除的缓存,属于活跃内存,又会导致去数据库查询,适得其反。
所以我们需要一种淘汰策略,针对百万级用户,日活20W的用户量来说:我们缓存最近活跃的30W用户的信息,那么不就完美的解决了上面的两点了吗?
实现思路
好了我们现在需求有了,缓存最近活跃的30W用户的信息。但是如何实现呢?
需求:我们需要一种容器,这种容器它是定容的(30W容量),同时它又是有序的(针对用户插入容器的顺序,节点越再后面,说明节点越新),同时它又有很高的查询性能。
LRU算法其实就是解决我们上面这个需求的,LRU全程Least Recently Used
,字面意思就是最近最少使用
,通常用于缓存的淘汰策略实现,因为内存资源非常宝贵,所以需要根据某种规则定期清除防止内存被撑满。
LRU算法的精妙之处是因为它采用了一种了LinkedHashMap
的哈希链表,它是一种有序的哈希表,使之前物理和逻辑上无序的哈希表,再给每个节点加入了一个,加入了一个前驱
和后继
指针后,便形成了逻辑上的连续,我们通过定义的一个头结点就可以依次的遍历整个哈希表,如下图(图片来自:https://www.cnblogs.com/xiaoxi/p/6170590.html):
执行流程
LRU算法具体是怎么实现的呢,我们以用户信息为例,来进行演示一下:
-
我们现在有一个容量为5的哈希链表,第一次依次插入5个用户
-
这时候我们需要访问id为002的用户信息,哈希链表首先把id为002的用户从哈希链表中移除,之后添加到哈希表的最末端
-
这时候,我们需要查询006用户的信息,但是006用户的信息哈希链表中没有,我们从数据库中查询出来之后添加到哈希链表的最末端。但是这个时候超出了哈希链表的最大容量5,那么就需要移除哈希链表最左端的节点,使之不会超出最大容量。
这样我们就实现了一个容量为5,而最右边是最近使用节点的一种容器,这就是我们LRU算法的基本思路
具体实现
LRU算法也有很多的实现的,上文也说了jdk中自带的LinkedHashMap
是天然的最近最少
模型容器,但是LinkedHashMap
不是线程安全的,在使用的注意线程安全问题。
对于用户系统Redis
也给我们提供了类似LRU
收集算法的实现。