你应该知道的缓存进化史( 二 )


你应该知道的缓存进化史

文章插图
 
 
你应该知道的缓存进化史

文章插图
 
 
在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中你可以如下面的代码一样,轻松使用:
你应该知道的缓存进化史

文章插图
 
 
我将会从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中的元素发生变化时进行调用,比如插入操作,更新操作,加载数据操作 。


推荐阅读