
文章插图

文章插图
在LinkedHashMap中维护了一个entry(用来放key和value的对象)链表 。在每一次get或者put的时候都会把插入的新entry,或查询到的老entry放在我们链表末尾 。可以注意到我们在构造方法中,设置的大小特意设置到max*1.4,在下面的removeEldestEntry方法中只需要size>max就淘汰,这样我们这个map永远也走不到扩容的逻辑了,通过重写LinkedHashMap,几个简单的方法我们实现了我们的LruMap 。
5、现代社会 - Guava cache在近代社会中已经发明出来了LRUMap,用来进行缓存数据的淘汰,但是有几个问题:
- 锁竞争严重,可以看见我的代码中,Lock是全局锁,在方法级别上面的,当调用量较大时,性能必然会比较低 。
- 不支持过期时间
- 不支持自动刷新

文章插图
我将会从guava cache原理中,解释guava cache是如何解决LRUMap的几个问题的 。
5.1、锁竞争
guava cache采用了类似ConcurrentHashMap的思想,分段加锁,在每个段里面各自负责自己的淘汰的事情 。在Guava根据一定的算法进行分段,这里要说明的是,如果段太少那竞争依然很严重,如果段太多会容易出现随机淘汰,比如大小为100的,给他分100个段,那也就是让每个数据都独占一个段,而每个段会自己处理淘汰的过程,所以会出现随机淘汰 。在guava cache中通过如下代码,计算出应该如何分段 。

文章插图
上面segmentCount就是我们最后的分段数,其保证了每个段至少10个Entry 。如果没有设置concurrencyLevel这个参数,那么默认就会是4,最后分段数也最多为4,例如我们size为100,会分为4段,每段最大的size是25 。在guava cache中对于写操作直接加锁,对于读操作,如果读取的数据没有过期,且已经加载就绪,不需要进行加锁,如果没有读到会再次加锁进行二次读,如果还没有需要进行缓存加载,也就是通过我们配置的CacheLoader,我这里配置的是直接返回Key,在业务中通常配置从数据库中查询 。如下图所示:

文章插图
5.2、过期时间
相比于LRUMap多了两种过期时间,一个是写后多久过期expireAfterWrite,一个是读后多久过期expireAfterAccess 。很有意思的事情是,在guava cache中对于过期的Entry并没有马上过期(也就是并没有后台线程一直在扫),而是通过进行读写操作的时候进行过期处理,这样做的好处是避免后台线程扫描的时候进行全局加锁 。看下面的代码:

文章插图
从这个结果中我们知道,在put的时候才进行的过期处理 。特别注意的是我上面concurrencyLevel(1)我这里将分段最大设置为1,不然不会出现这个实验效果的,在上面一节中已经说过,我们是以段位单位进行过期处理 。在每个Segment中维护了两个队列:

文章插图
writeQueue维护了写队列,队头代表着写得早的数据,队尾代表写得晚的数据 。accessQueue维护了访问队列,和LRU一样,用来我们进行访问时间的淘汰,如果当这个Segment超过最大容量,比如我们上面所说的25,超过之后,就会把accessQueue这个队列的第一个元素进行淘汰 。

文章插图
上面就是guava cache处理过期Entries的过程,会对两个队列一次进行peek操作,如果过期就进行删除 。一般处理过期Entries可以在我们的put操作的前后,或者读取数据时发现过期了,然后进行整个Segment的过期处理,又或者进行二次读lockedGetOrLoad操作的时候调用 。

文章插图
【你应该知道的缓存进化史】上面是我们驱逐Entry的时候的代码,可以看见访问的是accessQueue对其队头进行驱逐 。而驱逐策略一般是在对segment中的元素发生变化时进行调用,比如插入操作,更新操作,加载数据操作 。
推荐阅读
- 快收藏! 30 分钟包你学会 AWK
- 5款茶有效助你减掉腹部脂肪
- 律师|疫情之下,你知道赚钱的职业还有哪些吗?
- 一 鐘乳酒的功效与作用
- 豬膽酒的功效与作用
- 衰老|不化妆出门是一种什么感觉?
- 冬季饮食的五大误区,你中招了吗?
- 8款保健茶助你轻松下火治溃疡
- 汽车打不着火怎么办?除了花钱救援,老司机告诉你几个小妙招
- 5大养生花草茶助你春季驱寒邪
