1. 背景
java缓存道路的发展
图中分为几个阶段:
第一阶段: 数据同步加Redis
通过[消息队列]进行数据同步至[Redis],然后Java应用直接去取缓存
优点:由于使用的【分布式缓存】,数据更新快。
缺点:依赖Redis的稳定性,一旦Redis挂了,整个缓存系统不可用,造成缓存雪崩,所有请求都到DB。
第二、三阶段:Java Map到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
在HashMap中的问题是无法进行数据淘汰,导致内存无限增长,显然这样是不可取的。
有人就说我把一些数据给淘汰掉呗,这样不就对了,但是怎么淘汰呢?随机淘汰吗?当然不行
试想一下你刚把A装载进缓存,下一次要访问的时候就被淘汰了,那又会访问我们的数据库了,那我们要缓存干嘛呢?
几种淘汰算,下面列举常见的三种FIFO,LRU,LFU(还有一些ARC,MRU感兴趣的可以自行搜索):
-
FIFO:先进先出,在这种淘汰算法中,先进入缓存的会先被淘汰。这种可谓是最简单的了,但是会导致我们命中率很低。试想一下我们如果有个访问频率很高的数据是所有数据第一个访问的,而那些不是很高的是后面再访问的,那这样就会把我们的首个数据但是他的访问频率很高给挤出。
-
LRU:最近最少使用算法。在这种算法中避免了上面的问题,每次访问数据都会将其放在我们的队尾,如果需要淘汰数据,就只需要淘汰队首即可。但是这个依然有个问题,如果有个数据在1个小时的前59分钟访问了1万次(可见这是个热点数据),再后一分钟没有访问这个数据,但是有其他的数据访问,就导致了我们这个热点数据被淘汰。
-
LFU:最近最少频率使用。在这种算法中又对上面进行了优化,利用额外的空间记录每个数据的使用频率,然后选出频率最低进行淘汰。这样就避免了LRU不能处理时间段的问题。
上面列举了三种淘汰策略,对于这三种,实现成本是一个比一个高,同样的命中率也是一个比一个好。
而我们一般来说选择的方案居中即可,即实现成本不是太高,而命中率也还行的LRU。
如何实现一个LRUMap呢?我们可以通过继承LinkedHashMap,重写removeEldestEntry方法,即可完成一个简单的LRUMap。
。。。。。。。。。待实现。。。。。。。
5.现代社会——Guava Cache
在现代社会中已经发明出来了LRUMap,用来进行缓存数据的淘汰,但是有几个问题:
-
锁竞争严重,可以看见我的代码中,Lock是全局锁,在方法级别上面的,当调用量较大时,性能必然会比较低。
-
不支持过期时间
-
不支持自动刷新
所以谷歌的大佬们对于这些问题,按捺不住了,发明了Guava cache,在Guava cache中你可以如下面的代码一样,轻松使用:
public static void main(String [] args) throws ExecutionException { LoadingCache<String, String> cache = CacheBuilder.newBuilder() .maximumSize(100) //写之后30s过期 .expireAfterWrite(30L, TimeUnit.MILLISECONDS) //访问之后30s过期 .expireAfterAccess(30L, TimeUnit.MILLISECONDS) //20s秒之后刷新 .refreshAfterWrite(20L, TimeUnit.MILLISECONDS) //开启weakkey,当启动垃圾回收时,该缓存也被回收 .weakKeys() .build(createCacheLoader()); System.out.println(cache.get("hello")); cache.put("hello1", "我是hello1"); System.out.println(cache.get("hello1")); cache.put("hello1", "我是hello2"); System.out.println(cache.get("hello1")); } public static CacheLoader<String,String> createCacheLoader(){ return new CacheLoader<String, String>() { @Override public String load(String key) throws Exception { return key; } }; }
guava cache是如何解决LRUMap的几个问题的?
1. 锁竞争
2. 过期时间
3. 自动刷新
4. 其他特性
① 虚引用
② 删除监听器
guava cache的总结
Guava Cache其实就是一个性能不错的、api丰富的LRU Map。
可以通过对guava cache的二次开发,让其可以进行java应用服务之间的缓存更新。
6. 走向未来——caffeine(咖啡因)
guava cache的功能的确是很强大,满足了绝大多数的人的需求,但是其本质上还是LRU的一层封装。所以在众多其他较为优良的淘汰算法中就相形见绌了。
caffeine cache实现了W-TinyLFU(LFU+LRU算法的变种)。
https://mp.weixin.qq.com/s/y49CDMD0HASDJGDsBzjbPg