cache之guava
本文主要记录guava_cache的学习心得!
缓存是什么?为何要用缓存呢?
先参考下图!
这是一张小白图!简单形容了一个普普通通的服务端请求的处理模型! 当一个request请求通过网络不远千里的来到我们的机房! 首先nginx会给它找到一个合适的处理窗口,也就是我们的jvm进程,当jvm在进程空间内没有找到请求的结果,会尝试去分布式缓存中获取!如果分布式缓存中任然没有命中,再尝试去数据库中获取!
在上图中,a,b,c,d耗时逐渐增加,而且是跳跃式的增加!DB作为稳定性,安全性最值得依赖的存储空间,作为保障放在了最后一关!请求到数据库,需要通过漫长的网络和大量的磁盘io才能找到正确的数据!而又因为磁盘的io速度远远小于内存io,所以出现了分布式缓存!可是访问分布式缓存又隔着一层网络io,于是进程内部缓存应运而生!
以上三种角色,没有优劣之分,都在我们的系统中,承担了不可替代的重要角色!
讲到这里可以粗糙的理解为:缓存就是我们进程空间内部一片存储空间,是为了数据可以被进程更快速访问!
这里很多人会疑问,那实例个HashMap不就可以实现吗? 干嘛要提guava-cache!
这里就要说一下,机器的内存资源是十分宝贵的,内存相比磁盘的特点就是小而快!如果随便定义一块进程内的存储空间当cache,当原来越多的数据被放入,内存就又了爆掉的风险!而为这块进程内部存储空间包上了一层管理手段,使进程更合理更安全高效的使用内存,就是我们的缓存技术!为了更高效安全的使用内存,诸多缓存技术被开发出来,guava cache就是其中之一!
下面会从代码,去学习guava cache的使用和实现!
1 构建 (使用lodingcache作为示例)
guava cache的构建使用了简单易懂的构建者模式!核心参数如图:
maximumSize 最大存储数量,防止内存被过度使用而爆掉
expireAfterAccess 最后获取后的过期时间,无效数据剔除!
expireAfterWrite 写入后的过期时间,防止数据累积过多!同 expireAfterAccess 可二选一!
removalListener 数据移除时的监听器!
CacheLoader 数据加载器! 核心方法load(key), 在内存中没有获取到数据时会调用,将数据加载到cache中!
maximumSize ,expireAfterAccess ,expireAfterWrite 保证了内存空间使用的安全合理!
removalListener 和 CacheLoader 则提供了多样化的cache使用方式!
2 get方法 (核心方法)
可见,loadingCache的实现是LocalCache的静态内部类LocalLoadingCache,调用getOrLoad(key)来获取value!getOrLoad内部调用get(key,loader)方法!
在get方法内部,可以看到,guava cache使借鉴了jdk 1.8版本之前的ConcurrentHashMap,使用segment分段的方式,保证在多线程情景下的高效安全访问!
上图的get方法实现中可以看到!首先会判断 缓存中元素数量!
如果缓存中有数据,count!=0,通过key和hash获取value,可见getEntry() -->getFirst(hash) ,其结构同jdk HashMap原理,使用 AtomicReferenceArray 作为table数组,根据hash定位下表,获取到目标链表,然后遍历链表,比对key值,尝试获取value!
如果获取到value,会使用recordRead方法记录当前时间戳为最后获取此key的时间,读取此key的次数+1!在返回value时,会调用 scheduleRefresh()刷新任务方法返回!这里也可以看到cache包装的强大功能,根据当前时间now减value的写入时间戳的结果是否大于刷新时间间隔,来判断是否需要刷新!
是,则使用loader加载器加载新值,刷新缓存并返回!否,则旧值返回!
如果缓存中没有数据,count==0 ,也会执行 lockedGetOrLoad()方法!如下:
依然是根据hash,定位到index下标,如果找到下标,则循环遍历链表,比对key值,获取value!其中,会判断获取到的value,如果是null,塞入队列,等待异步清除!
如果不是null,也要判断是否过期,在过期的情况下,依然会塞入异步清除队列!
如果没有获取到value的情况下,判断是否需要创建新的entry,既createNewEntry==true,会创建一个弱引用或软引用,LocalCache.LoadingValueReference(),放进table中,最后调用load()方法,为该引用加载数据!
注:判断数据是否过期的方法isExpired()
如果设置的是写入后过期,则拿当前时间减写入时间和过期时间比较大小,
如果是最后一次获取后过期,则拿当前时间减最后一次access时间 和过期时间比较大小!
其上为cache的核心方法get()的大概实现!
总价概括为:cache使用了segment分段锁控制了并发写入,使用了数组+hash+链表的数据结构实现了高效读写,数组是juc大神dog.lea写的原子性安全读写的AtomicReferenceArray,使用了引用队列存放key,value,保证了gc回收及时!recencyQueue 和 accessQueue (前者是读取是写入,后者是加锁状态下的写都会写入)保证了回收算法的异步高效实现!
由此可见,内存的使用,在服务端是一件严谨甚至严苛的事,使用得当,会给服务的效率带来极大的提升!使用不得当,内存的安全,进程的生命,就会立即受到致命的威胁!
本地缓存技术,越来越被重视。以下为几种常见cache技术的特性:
1 ehcache,使用简单,访问效率高,轻量接入, 适合数据更新少,并发低的场景下
2 guava cache 功能强大,配置多,支持三种淘汰策略,支持刷新,移除,记数等功能,支持弱引用 软引用, 适合有一定的内存空间的条件,频繁读写场景下!
3 jetcache 阿里开源,springboot支持,亲redis,多级缓存使用良好!
4 caffeine是guava的升级款,也是最近被主推的cache!使用方式同guava区别不大,但是性能上提升很大!