你应该知道的缓存进化史

作者:咖啡拿铁来源:https://www.jianshu.com/p/98a387bd92d81、背景本文是上周去技术沙龙听了一下爱奇艺的JAVA缓存之路有感写出来的 。先简单介绍一下爱奇艺的java缓存道路的发展吧 。

你应该知道的缓存进化史

文章插图
 
可以看见图中分为几个阶段:
  • 第一阶段:数据同步加redis
通过消息队列进行数据同步至redis,然后Java应用直接去取缓存 这个阶段优点是:由于是使用的分布式缓存,所以数据更新快 。缺点也比较明显:依赖Redis的稳定性,一旦redis挂了,整个缓存系统不可用,造成缓存雪崩,所有请求打到DB 。
  • 第二、三阶段:JavaMap到Guava cache
这个阶段使用进程内缓存作为一级缓存,redis作为二级 。优点:不受外部系统影响,其他系统挂了,依然能使用 。缺点:进程内缓存无法像分布式缓存那样做到实时更新 。由于java内存有限,必定缓存得设置大小,然后有些缓存会被淘汰,就会有命中率的问题 。
  • 第四阶段: Guava Cache刷新
为了解决上面的问题,利用Guava Cache可以设置写后刷新时间,进行刷新 。解决了一直不更新的问题,但是依然没有解决实时刷新 。
  • 第五阶段: 外部缓存异步刷新

你应该知道的缓存进化史

文章插图
 
  •  
这个阶段扩展了Guava Cache,利用redis作为消息队列通知机制,通知其他java应用程序进行刷新 。
这里简单介绍一下爱奇艺缓存发展的五个阶段,当然还有一些其他的优化,比如GC调优,缓存穿透,缓存覆盖的一些优化等等 。有兴趣的同学可以关注公众号,联系我进行交流 。
2、原始社会 - 查库上面说的是爱奇艺的一个进化线路,但是在大家的一般开发过程中,第一步一般都没有redis,而是直接查库 。
在流量不大的时候,查数据库或者读取文件是最为方便,也能完全满足我们的业务要求 。
3、古代社会 - HashMap当我们应用有一定流量之后或者查询数据库特别频繁,这个时候就可以祭出我们的java中自带的HashMap或者ConcurrentHashMap 。我们可以在代码中这么写:
你应该知道的缓存进化史

文章插图
 
 
但是这样做就有个问题HashMap无法进行数据淘汰,内存会无限制的增长,所以hashMap很快也被淘汰了 。当然并不是说他完全就没用,就像我们古代社会也不是所有的东西都是过时的,比如我们中华名族的传统美德是永不过时的,就像这个hashMap一样的可以在某些场景下作为缓存,当不需要淘汰机制的时候,比如我们利用反射,如果我们每次都通过反射去搜索Method,field,性能必定低效,这时我们用HashMap将其缓存起来,性能能提升很多 。
4、近代社会 - LRUHashMap在古代社会中难住我们的问题无法进行数据淘汰,这样会导致我们内存无限膨胀,显然我们是不可以接受的 。有人就说我把一些数据给淘汰掉呗,这样不就对了,但是怎么淘汰呢?随机淘汰吗?当然不行,试想一下你刚把A装载进缓存,下一次要访问的时候就被淘汰了,那又会访问我们的数据库了,那我们要缓存干嘛呢?
所以聪明的人们就发明了几种淘汰算法,下面列举下常见的三种FIFO,LRU,LFU(还有一些ARC,MRU感兴趣的可以自行搜索):
  • FIFO:先进先出,在这种淘汰算法中,先进入缓存的会先被淘汰 。这种可谓是最简单的了,但是会导致我们命中率很低 。试想一下我们如果有个访问频率很高的数据是所有数据第一个访问的,而那些不是很高的是后面再访问的,那这样就会把我们的首个数据但是他的访问频率很高给挤出 。
  • LRU:最近最少使用算法 。在这种算法中避免了上面的问题,每次访问数据都会将其放在我们的队尾,如果需要淘汰数据,就只需要淘汰队首即可 。但是这个依然有个问题,如果有个数据在1个小时的前59分钟访问了1万次(可见这是个热点数据),再后一分钟没有访问这个数据,但是有其他的数据访问,就导致了我们这个热点数据被淘汰 。
  • LFU:最近最少频率使用 。在这种算法中又对上面进行了优化,利用额外的空间记录每个数据的使用频率,然后选出频率最低进行淘汰 。这样就避免了LRU不能处理时间段的问题 。
上面列举了三种淘汰策略,对于这三种,实现成本是一个比一个高,同样的命中率也是一个比一个好 。而我们一般来说选择的方案居中即可,即实现成本不是太高,而命中率也还行的LRU,如何实现一个LRUMap呢?我们可以通过继承LinkedHashMap,重写removeEldestEntry方法,即可完成一个简单的LRUMap 。


推荐阅读