guava缓存

guava缓存介绍:

guava缓存是谷歌开源的一种本地缓存,缓存是使用本机的内存来存储的,实现原理类似于ConcurrentHashMap

实现原理

看guava cache的代码可以发现,他的实现是和java老版本的concurrentHashMap有点相似的。
在localcache这个类中,实现了核心的数据结构和算法。继承abstractMap,实现concurrentMap接口,持有一个segment数组,segment继承自reentrantLock,有keyRederenceQueue队列,valueReferenceQueue队列,用于快速回收。recencyQueue用于记录访问了哪些节点来更新访问列表的顺序,在大于清除阈值或segment写时被清理;还有writeQueue按照写入时间排序,在写时添加到队尾;accessQueue按照访问时间排序,在访问时添加到队尾

ReferenceEntry数据结构用来记录节点数据,这种数据结构可以支持按照强引用或弱引用来生成key;recency队列用于实现lru算法;write和access这两个队列都是双向连表,用于实现最近最少使用算法。

使用方式
guava cache在创建时提供了两种创建方式

Loading cache
可以理解为在build cache的时候就定义好了缓存CacheLoader里的load方法,缓存中不存在时会去调用这个load方法加载数据

Callable cache
和loading cache不同的是,没有在build时传入CacheLoader,而是在使用get的时候传入一个callable对象来获取数据

这两种使用方式的原则都是在get操作时优先从缓存里读取数据,当数据不存在时通过cacheloader尝试加载数据写入缓存,callable cache的方式更灵活,loading cache的方式写起来更简单。

为什么要用缓存

为了系统的高并发,高性能,提高访问速度

应用

//实例化缓存构建器 CacheBuildercacheBuilder=CacheBuilder.newBuilder(); //构建缓存容器 Cachecache=cacheBuilder.build(); //缓存数据 cache.put(“cache”,“cache-value”); //获取缓存数据 Stringvalue=cache.getIfPresent(“cache”); //删除缓存 cache.invalidate(“cache5”);

参数

maximumSize
设置构建缓存容器的最大容量,当缓存数量达到该容量时,会删除其中的缓存(根据实际测试结果,会先删除先放入的数据)
concurrencyLevel
设置并发等级,也就是同时操作缓存的线程数。默认是4。在guava cache的实现中,会根据这个值创建一个希表段,每个哈希表段由其自己的写锁控制。每次显式写入都会使用一次段锁,每次缓存加载计算都会使用两次段锁(一次在加载新值之前,一次在加载完成之后),许多内部缓存管理是在段粒度上执行的。
expireAfterWrite
缓存写入多久后过期,这个方法和我们前面分享的Caffeine是一样的。
expireAfterAccess
访问后多久过期,这个方法也和Caffeine是一样的。

refreshAfterWrite
缓存写入多久后刷新,这个方法也和Caffeine的一样,这几个方法应该就是Caffeine文档中所说的设计借鉴吧。

使用场景

消耗内存空间来提升速度(时间换空间)
某些键会呗查询多次
缓存中存放的数据总量不会超出内存的容量

回收触发时机,缓存过期

按照以往对一些其他数据结构的了解,缓存过期策略的实现不外乎应该也是在读时判断是否超时,或者定时清理,鉴于这是个基础工具,应该不会去做定时清理这种重量级操作,所以应该还是在get的时候去判断是否超时,至于是根据写入时间还是根据读取时间,在这个框架里也是可以配置的,通过expireAfterWrite和expireAfterAccess

通过看代码发现,这其实是比较简单的,entry节点记录了accessTime和writeTime,在根据key get value的时候,实现了一个getLiveValue方法,在这个方法里判断过期策略,以及对比当前时间和节点过期时间,而这个时间分别是在写和读节点时通过recordWrite和recordRead方法写入的。

并发问题

总算到了重点,那么并发情况下会有什么问题呢。cache本身的实现是线程安全的,那么并发下存在的问题可能就是加锁导致的阻塞了。

当尝试通过get获取缓存值的时候,如果不存在key或者key已经过期,这时候需要去执行load方法加载数据,为了保证线程安全,这个方法会对segment加锁,在这种锁粒度下,其他尝试获取这个key的线程就会被waitForValue方法阻塞,如果加载速度过慢就会有大量的线程被阻塞。

解决方式
其实关于并发问题的解决是有通用方法的,当看到guava cache解决这些问题的方法的时候,不自主的就想到了之前看过的一些其他代码和解决方案。读多写少,热key问题,这几个关键词就自己浮现了出来。随便提一个,并发安全的读多写少的数据结构还有一种是copyonwrite,通过读写分离来实现,写操作完成前读使用旧值。

在guava cache下,可以通过创建时配置refreshAfterWrite这个参数,定义在写入多久后刷新数据,在满足条件时只阻塞加载数据的线程,其余线程直接返回旧数据。

缓存雪崩
即使使用refreshAfterWrite这种配置方式,当多个缓存到达刷新条件时,如果同时对这些key进行请求,依然会有多个线程会被阻塞,造成大量线程阻塞,并且他们会同时去调用load方法请求资源,造成服务端的压力。

这时可以通过重写实现CacheLoader的reload方法,把加载数据的任务提交到线程池,这时用户请求不会被阻塞,会直接返回旧值,执行完成后而且线程池能够控制对资源的访问,不会对数据库等造成过大的压力。

你可能感兴趣的:(缓存,java,开发语言)