概述:

本次主要是分析cache的源码,基本概念官方简介即可。

基本类图:

在官方的文档说明中,Guava Cache实现了三种加载缓存的方式:

  • LoadingCache在构建缓存的时候,使用build方法内部调用CacheLoader方法加载数据
  • 在使用get方法的时候,如果缓存不存在该key或者key过期等,则调用get(K, Callable)方式加载数据;
  • 直接调用put方法来放置缓存

核心类及接口的说明,简单的理解如下:

  • Cache接口是Guava对外暴露的缓存接口,对外的方法如下图,Cache定义的接口get接口中,必须要传入Callable,Callable是如果不存在就加载的方式定义,这种就是第二种加载缓存的方式,如果缓存中key不存在或者过期的情况,调用get(K,Callable)来实现

  • LoadingCache是对Cache的进一步封装,继承自Cache接口,主要是实现了get(K)这种定义策略

  • LocalManualCache实际上是Cache的标准实现,注意LocalManualCache不包含无Callable参数的get方法,是一种能在键值找不到的时候手动调用获取值的方式

  • LocalLoadingCache则是LoadingCache的实现,核心的区别在于支持在key找不到的情况下自动加载value的功能点,其实是保存了一个CacheLoading的初始值

  • LocalCache是存储层,是真正意义上数据存放的地方,继承了java.util.AbstractMap同时也实现了ConcurrentMap接口,实现方式和ConcurrentHashMap的实现相同,都是采用分segment来细化管理HashMap中的节点Entry,细粒度锁的方式来增大并发性能。

  • CacheLoader我的理解是缓存加载策略,即负责计算key-value的对应关系,是一个抽象类,需要业务定制自己的策略。在Guava的使用过程中,get参数传入的Callable接口最终会被封装成匿名的CacheLoader,负责加载key到缓存中

  • CacheBuilder 由于cache配置项众多,典型的builder模式场景,复杂对象的构造与其对应配置属性表示的分离。

实现详解:

底层存储LocalCache

LocalCache是线程安全的集合,为了实现这个特性,使用了经典的细粒度锁来控制,本质和ConcurrentHashMap的实现方式类型,在存储中采用了多个Segment对应一个锁,来分散全局锁带来的性能损失。当去put一个entry的时候,一般只需要拥有某一个segment锁就可以完成。下图是ConcurrentHashMap和HashTable存储的描述。

在实现上,LocalCache的并发策略和ConcurrentHashMap的并发策略一致,也是进行了分段,支持不同段的并发写入。

  • Segment中使用 volatile AtomicReferenceArray<ReferenceEntry<K, V>> table;

    来存储对象,可以这样理解,每一段的Segment相当于一个HashTable的实现
  • guava的实例对象中存在一些Queue,这个是Guava扩展实现的各种引用对象回收的策略(Strong、Weak、Soft)类型,这块不具体分析了,平时我并不怎么用Weak、Soft,接触过的只有ThreadLocal里面,这块可以看我博客fail-fast分析。同时,Guava定义了ReferenceEntry,ValueReference也是为GC回收策略做的
  • Segment中的table是一个array,每一个元素都是RefenceEntry的链表,同时会将具体的value值封装为一个ValueReference

  • LocalCache的扩容是基于Segment的,也就是说是分片的扩展,单个Segment只需要关注自己的容量,与其他的Segment无关的
  • Segment中使用的是LRU缓存回收算法,GuavaCache实现的LRU针对的是Segment来做回收的,不是针对整个LocaCache来做的

ReferenceEntry及LRU回收算法的实现

ReferenceEntry是Guava中对一个key-value节点的抽象,每一个Segment中都包含这一个ReferenceEntry数组,每个ReferenceEntry数组项都是一条ReferenceEntry链,其数据结构如下:

类继承结构如下:

ReferenceEntry包装了key-value节点的同时,主要的功能点是增加了引用数据类型回收机制(这个不讨论),设置了accessQueue和writeQueue队列,这个两个其实是双向链表,分别通过previousAccess、nextAccess和previousWrite、nextWrite字段链接而成,这两个队列存在的目的是:实现LRU算法

涉及到一些概念说明:

  • accessQueue:这个队列是按照最久未使用的顺序存放的缓存对象(ReferenceEntry)的。由于会经常进行元素的移动,例如把访问过的对象放到队列的最后。
  • writeQueue:保存按照写入缓存先后时间的队列,对于新写入的节点或者更新的节点,都会将该节点ReferenceEntry加入到队尾。对头元素是长时间没有变化的对象,而队尾则是最近更新的节点。
  • recencyQueue:每次访问操作(即客户端每次调用get方法的时候)都会将该entry加入到队列尾部,并更新accessTime。如果遇到写入操作,则将给队列内容排干,如果accessQueue队列中持有该这些 entry,然后将这些entry add到accessQueue队列。如此看来,recencyQueue是为 accessQueue服务的,一开始也不是很明白为什么有了accessQueue还有设置recencyQueue,下面的链接做了解释,简单讲就是get的使用使用了ConcurrentLinkedQueue来记录访问的数据,这样的好处是不需要lock()

https://github.com/google/guava/issues/1487

put操作

对于Segment的put,基本流程如下:

  • 加锁lock
  • 判断缓存是否超过了过期时间
  • 判断当前存储是否到了最大容量,如果到了,扩容
  • 将新元素进行封装,加入到存储中
  • 更新控制队列,accessQueue、writeQueue
  • 判断队列中现有元素是否超过了maximumSize,进行容量的控制
  • 触发时间通知,包括StatsCounter和RemovalNotification
  • 释放锁

Segment对锁的控制

上面提到过LocalCacal对于并发的控制,粒度是Segment级别,而Segment当中锁的操作相对来说比较频繁,在设计的时候,为了简单,直接让Segment继承了java.util.concurrent.locks.ReentrantLock

segment对size的控制策略

guava cache并不会开启额外的线程去扫描当前的存储,看是否达到了存储上限,而是在每次put的时候进行判断

/**
* Performs eviction if the segment is full. This should only be called prior to adding a new
* entry and increasing {@code count}.
*/
@GuardedBy("Segment.this")
void evictEntries() {
if (!map.evictsBySize()) {
return;
} drainRecencyQueue();
while (totalWeight > maxSegmentWeight) {
ReferenceEntry<K, V> e = getNextEvictable();
if (!removeEntry(e, e.getHash(), RemovalCause.SIZE)) {
throw new AssertionError();
}
}
} // TODO(fry): instead implement this with an eviction head
ReferenceEntry<K, V> getNextEvictable() {
for (ReferenceEntry<K, V> e : accessQueue) {
int weight = e.getValueReference().getWeight();
if (weight > 0) {
return e;
}
}
throw new AssertionError();
}

之前有说到过accessQueue,这个队列是按照最久未使用的顺序存放的缓存对象(ReferenceEntry)的。由于会经常进行元素的移动,例如把访问过的对象放到队列的最后。而当元素超过了预设的maximumSize,就会从accessQueue的队头取对应的数据,也就是最长时间没有访问到的那个元素,然后从Segment的table中剔除,同样的也要从writeQueue、accessQueue中剔除

ReferenceEntry<K, V> removeValueFromChain(ReferenceEntry<K, V> first,
ReferenceEntry<K, V> entry, @Nullable K key, int hash, ValueReference<K, V> valueReference,
RemovalCause cause) {
enqueueNotification(key, hash, valueReference, cause);
writeQueue.remove(entry);
accessQueue.remove(entry); if (valueReference.isLoading()) {
valueReference.notifyNewValue(null);
return first;
} else {
return removeEntryFromChain(first, entry);
}
}

Segment对失效时间expireTime的控制

segment对失效时间的控制也并不是由单独的线程去控制,而是在用户每次请求的时候触发检测,这样可以有效的避免不必要的线程消耗。但是这样也会有一定的问题,简单讲,如果大量的请求同时到,而且缓存内容全部都失效的话,相当于没有做任何缓存控制,而且还延长了单次请求的时间。在大促的时候曾经遇到过,每隔一段时间都发现请求rt会出现毛刺,后来发现是用来本地缓存,大量的数据同时失效,而且恰好有很多请求同时来到,全部都去读取DB,rt全部变高。

这种方式也临时的解决方案,比如说,预热缓存的时候分批进行。

如果真的存在一些数据需要常驻本地缓存,可以考虑使用额外的线程进行定时刷新,简单的做法是:假设设置的expireTime为10分钟,那么每隔9分钟,定时任务去读取cache中的数据,然后更新。(之前看zk的代码,zkserver对于client Session的控制是单独线程控制的,那个实现感觉是比较经典的,如果有必要做成是开启额外线程失效的话,可以参考那个实现)。

失效的代码如下,和对数量的控制没大的区别:

   void expireEntries(long now) {
drainRecencyQueue(); ReferenceEntry<K, V> e;
while ((e = writeQueue.peek()) != null && map.isExpired(e, now)) {
if (!removeEntry(e, e.getHash(), RemovalCause.EXPIRED)) {
throw new AssertionError();
}
}
while ((e = accessQueue.peek()) != null && map.isExpired(e, now)) {
if (!removeEntry(e, e.getHash(), RemovalCause.EXPIRED)) {
throw new AssertionError();
}
}
}

其他功能点:

StatsCounter和CacheStatus

为了纪录Cache的使用情况,如果命中次数、没有命中次数、evict次数等,Guava Cache中定义了StatsCounter做这些统计信息,它有一个简单的SimpleStatsCounter实现,我们也可以通过CacheBuilder配置自己的StatsCounter。

RemoveListener

put和get操作后都会通知removeListener,默认是同步的方式处理事件通知。也可以通过RemoveListeners将 listener包装成异步方式处理

参考文章:

  1. https://github.com/google/guava/wiki/CachesExplained
  2. http://www.blogjava.net/DLevin/archive/2013/10/20/404847.html
  3. https://github.com/google/guava/issues/1487

Guava Cache源码解析的更多相关文章

  1. Google guava cache源码解析1--构建缓存器(1)

    此文已由作者赵计刚授权网易云社区发布. 欢迎访问网易云社区,了解更多网易技术产品运营经验. 1.guava cache 当下最常用最简单的本地缓存 线程安全的本地缓存 类似于ConcurrentHas ...

  2. 第二章 Google guava cache源码解析1--构建缓存器

    1.guava cache 当下最常用最简单的本地缓存 线程安全的本地缓存 类似于ConcurrentHashMap(或者说成就是一个ConcurrentHashMap,只是在其上多添加了一些功能) ...

  3. Google guava cache源码解析1--构建缓存器(2)

    此文已由作者赵计刚授权网易云社区发布. 欢迎访问网易云社区,了解更多网易技术产品运营经验. CacheBuilder-->maximumSize(long size)     /**       ...

  4. Google guava cache源码解析1--构建缓存器(3)

    此文已由作者赵计刚授权网易云社区发布. 欢迎访问网易云社区,了解更多网易技术产品运营经验. 下面介绍在LocalCache(CacheBuilder, CacheLoader)中调用的一些方法: Ca ...

  5. Guava Cache源码详解

    目录 一.引子 二.使用方法 2.1 CacheBuilder有3种失效重载模式 2.2 测试验证 三.源码剖析 3.1 简介 3.2 源码剖析 四.总结 优点: 缺点: 正文 回到顶部 一.引子 缓 ...

  6. 常用限流算法与Guava RateLimiter源码解析

    在分布式系统中,应对高并发访问时,缓存.限流.降级是保护系统正常运行的常用方法.当请求量突发暴涨时,如果不加以限制访问,则可能导致整个系统崩溃,服务不可用.同时有一些业务场景,比如短信验证码,或者其它 ...

  7. Guava Cache源码浅析

    1. 简介 Guava Cache是指在JVM的内存中缓存数据,相比较于传统的数据库或redis存储,访问内存中的数据会更加高效,无网络开销. 根据Guava官网介绍,下面的这几种情况可以考虑使用Gu ...

  8. Spring Cache 源码解析

    这个类实现了Spring的缓存拦截器 org.springframework.cache.interceptor.CacheInterceptor @SuppressWarnings("se ...

  9. 第十三章 ThreadPoolExecutor源码解析

    ThreadPoolExecutor使用方式.工作机理以及参数的详细介绍,请参照<第十二章 ThreadPoolExecutor使用与工作机理 > 1.源代码主要掌握两个部分 线程池的创建 ...

随机推荐

  1. 谷歌是如何做代码审查的 | 外刊IT评论 - Google Chrome

    谷歌是如何做代码审查的           本文的作者 Mark CC 在上一篇文章中提到过,我已经不在Google工作了.我还没有想清楚应该去哪里-有两三个非常好的工作机会摆在我面前.因为在这段做决 ...

  2. Java IO在实际开发中的应用

    IO是java绕不过去的槛,在开发中io无处不在, 正如同 世界上本没有路,java io写多了,也就知道了大体是什么意思,在读完thinking in java 感觉就更清晰了,结合具体的业务场景, ...

  3. C#.NET 中visual studio生成的.pdb/ .vshost.exe/ .vshost.exe.manifest文件是什么

      pdb文件: 英文全称:Program Database File 中文全称:程序数据库 文件 Debug里的PDB是full,保存着调试和项目状态信息.有断言.堆栈检查等代码.可以对程序的调试配 ...

  4. 使用jenkins实现持续集成

    一.jenkins 介绍 它是一个自动化的周期性的集成测试过程,从检出代码.编译构建.运行测试.结果记录.测试统计等都是自动完成的,无需人工干预: 它需要有专门的集成服务器来执行集成构建: 它需要有代 ...

  5. SSO(单点登录)与旅游年卡

    SSO(单点登录)与旅游年卡 SSO英文全称Single Sign On,单点登录.SSO是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统.它包括可以将这次主要的登录映射到其他应 ...

  6. Nodejs cluster模块深入探究

    由表及里 HTTP服务器用于响应来自客户端的请求,当客户端请求数逐渐增大时服务端的处理机制有多种,如tomcat的多线程.nginx的事件循环等.而对于node而言,由于其也采用事件循环和异步I/O机 ...

  7. java三大框架介绍

    常听人提起三大框架,关于三大框架,做了如下了解: 三大框架:Struts+Hibernate+Spring java三大框架主要用来做WEN应用. Struts主要负责表示层的显示 Spring利用它 ...

  8. linux DNS 问题

    今天准备爬虫51job时候,发现ping不通外网了,ping 了一下IP,都是OK的,只是host不通. 呵呵,就一DNS问题,好的.第一步,开始检查配置文件 cat /etc/sysconfig/n ...

  9. jmeter之BeanShell对两个变量断言对比

    在jmeter的中,断言没法对两个变量的进行对比后判断,只能使用Bean Shell断言来进行. 假设需求: 获取某类型用户uid个数与数据库查询结果是否相等 获取uid个数用http接口获取统计数据 ...

  10. SQL Server事务遭遇网络异常时的处理机制浅析

    SQL Server数据库中,如果应用程序正在执行一个事务的时候突然遭遇了网络异常,例如网络掉包,网络中断等,那么这个事务会怎么样? SQL Server数据库是通过什么机制来判断处理呢? 估计很多人 ...