• guava cache的优点和使用场景,用来判断业务中是否适合使用此缓存
  • 介绍常用的方法,并给出示例,作为使用的参考
  • 深入解读源码。

guava简介

guava cache是一个本地缓存。有以下优点:

  • 很好的封装了get、put操作,能够集成数据源。

    一般我们在业务中操作缓存,都会操作缓存和数据源两部分。如:put数据时,先插入DB,再删除原来的缓存;ge数据时,先查缓存,命中则返回,没有命中时,需要查询DB,再把查询结果放入缓存中。 guava cache封装了这么多步骤,只需要调用一次get/put方法即可。
  • 线程安全的缓存,与ConcurrentMap相似,但前者增加了更多的元素失效策略,后者只能显示的移除元素。
  • Guava Cache提供了三种基本的缓存回收方式:基于容量回收、定时回收和基于引用回收。定时回收有两种:按照写入时间,最早写入的最先回收;按照访问时间,最早访问的最早回收。
  • 监控缓存加载/命中情况。

常用方法

  • V getIfPresent(Object key) 获取缓存中key对应的value,如果缓存没命中,返回null。return value if cached, otherwise return null.
  • V get(K key) throws ExecutionException 获取key对应的value,若缓存中没有,则调用LocalCache的load方法,从数据源中加载,并缓存。 return value if cached, otherwise load, cache and return.
  • void put(K key, V value) if cached, return; otherwise create, cache , and return.
  • void invalidate(Object key); 删除缓存
  • void invalidateAll(); 清楚所有的缓存,相当远map的clear操作。
  • long size(); 获取缓存中元素的大概个数。为什么是大概呢?元素失效之时,并不会实时的更新size,所以这里的size可能会包含失效元素。
  • CacheStats stats(); 缓存的状态数据,包括(未)命中个数,加载成功/失败个数,总共加载时间,删除个数等。
  • ConcurrentMap<K, V> asMap(); 将缓存存储到一个线程安全的map中。

批量操作就是循环调用上面对应的方法,如:

  • ImmutableMap<K, V> getAllPresent(Iterable<?> keys);
  • void putAll(Map<? extends K,? extends V> m);
  • void invalidateAll(Iterable<?> keys);

示例:

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import java.util.concurrent.TimeUnit; public class guavaSample { public static void main(String[] args) { LoadingCache<String, Integer> cache = CacheBuilder.newBuilder()
.maximumSize(10) //最多存放十个数据
.expireAfterWrite(10, TimeUnit.SECONDS) //缓存200秒
.recordStats() //开启 记录状态数据功能
.build(new CacheLoader<String, Integer>() {
//数据加载,默认返回-1,也可以是查询操作,如从DB查询
@Override
public Integer load(String key) throws Exception {
return -1;
}
}); //只查询缓存,没有命中,即返回null。 miss++
System.out.println(cache.getIfPresent("key1")); //null
//put数据,放在缓存中
cache.put("key1", 1);
//再次查询,已存在缓存中, hit++
System.out.println(cache.getIfPresent("key1")); //1
//失效缓存
cache.invalidate("key1");
//失效之后,查询,已不在缓存中, miss++
System.out.println(cache.getIfPresent("key1")); //null try{
//查询缓存,未命中,调用load方法,返回-1. miss++
System.out.println(cache.get("key2")); //-1
//put数据,更新缓存
cache.put("key2", 2);
//查询得到最新的数据, hit++
System.out.println(cache.get("key2")); //2
System.out.println("size :" + cache.size()); //1 //插入十个数据
for(int i=3; i<13; i++){
cache.put("key"+i, i);
}
//超过最大容量的,删除最早插入的数据,size正确
System.out.println("size :" + cache.size()); //10
//miss++
System.out.println(cache.getIfPresent("key2")); //null Thread.sleep(5000); //等待5秒
cache.put("key1", 1);
cache.put("key2", 2);
//key5还没有失效,返回5。缓存中数据为key1,key2,key5-key12. hit++
System.out.println(cache.getIfPresent("key5")); //5 Thread.sleep(5000); //等待5秒
//此时key5-key12已经失效,但是size没有更新
System.out.println("size :" + cache.size()); //10
//key1存在, hit++
System.out.println(cache.getIfPresent("key1")); //1
System.out.println("size :" + cache.size()); //10
//获取key5,发现已经失效,然后刷新缓存,遍历数据,去掉失效的所有数据, miss++
System.out.println(cache.getIfPresent("key5")); //null
//此时只有key1,key2没有失效
System.out.println("size :" + cache.size()); //2 System.out.println("status, hitCount:" + cache.stats().hitCount()
+ ", missCount:" + cache.stats().missCount()); //4,5
}catch (Exception e){
System.out.println(e.getMessage());
}
}
}

guava cache源码解析

先了解一些主要类和接口:

  • CacheBuilder:类,缓存构建器。构建缓存的入口,指定缓存配置参数并初始化本地缓存。

    CacheBuilder在build方法中,会把前面设置的参数,全部传递给LocalCache,它自己实际不参与任何计算。这种初始化参数的方法值得借鉴,代码简洁易读。
  • CacheLoader:抽象类。用于从数据源加载数据,定义load、reload、loadAll等操作。
  • Cache:接口,定义get、put、invalidate等操作,这里只有缓存增删改的操作,没有数据加载的操作。
  • AbstractCache:抽象类,实现Cache接口。其中批量操作都是循环执行单次行为,而单次行为都没有具体定义。
  • LoadingCache:接口,继承自Cache。定义get、getUnchecked、getAll等操作,这些操作都会从数据源load数据。
  • AbstractLoadingCache:抽象类,继承自AbstractCache,实现LoadingCache接口。
  • LocalCache:类。整个guava cache的核心类,包含了guava cache的数据结构以及基本的缓存的操作方法。
  • LocalManualCache:LocalCache内部静态类,实现Cache接口。其内部的增删改缓存操作全部调用成员变量localCache(LocalCache类型)的相应方法。
  • LocalLoadingCache:LocalCache内部静态类,继承自LocalManualCache类,实现LoadingCache接口。其所有操作也是调用成员变量localCache(LocalCache类型)的相应方法。

综上,guava cache的核心操作,都在LocalCache中实现。

其他:

  • CacheStats:缓存加载/命中统计信息。

在看具体的代码之前,先来简单了解一下LocalCache的数据结构。

LocalCache的数据结构如下所示:

LocalCache的数据结构与ConcurrentHashMap很相似,都由多个segment组成,且各segment相对独立,互不影响,所以能支持并行操作。每个segment由一个table和若干队列组成。缓存数据存储在table中,其类型为AtomicReferenceArray<ReferenceEntry<K, V>>,即一个数组,数组中每个元素是一个链表。两个队列分别是writeQueue和accessQueue,用来存储写入的数据和最近访问的数据,当数据过期,需要刷新整体缓存(见上述示例最后一次cache.getIfPresent("key5"))时,遍历队列,如果数据过期,则从table中删除。segment中还有基于引用场景的其他队列,这里先不做讨论。

CacheBuilder

CacheBuilder是缓存配置和构建入口,先看一些属性。CacheBuilder的设置操作都是为这些属性赋值。

    //缓存的默认初始化大小
private static final int DEFAULT_INITIAL_CAPACITY = 16;
// LocalCache默认并发数,用来评估Segment的个数
private static final int DEFAULT_CONCURRENCY_LEVEL = 4;
//默认的缓存过期时间
private static final int DEFAULT_EXPIRATION_NANOS = 0; static final int UNSET_INT = -1;
int initialCapacity = UNSET_INT;//初始缓存大小
int concurrencyLevel = UNSET_INT;//用于计算有几个并发
long maximumSize = UNSET_INT;//cache中最多能存放的缓存entry个数
long maximumWeight = UNSET_INT; Strength keyStrength;//键的引用类型(strong、weak、soft)
Strength valueStrength;//值的引用类型(strong、weak、soft) long expireAfterWriteNanos = UNSET_INT;//缓存超时时间(起点:缓存被创建或被修改)
long expireAfterAccessNanos = UNSET_INT;//缓存超时时间(起点:缓存被创建或被修改或被访问)
//元素被移除的监听器
RemovalListener<? super K, ? super V> removalListener;
//状态计数器,默认为NULL_STATS_COUNTER,即不启动计数功能
Supplier<? extends StatsCounter> statsCounterSupplie

CacheBuilder构建缓存有两个方法:

//构建一个具有数据加载功能的缓存
public <K1 extends K, V1 extends V> LoadingCache<K1, V1> build(
CacheLoader<? super K1, V1> loader) {
checkWeightWithWeigher();
//调用LocalCache构造方法
return new LocalCache.LocalLoadingCache<K1, V1>(this, loader);
} //构建一个没有数据加载功能的缓存
public <K1 extends K, V1 extends V> Cache<K1, V1> build() {
checkWeightWithWeigher();
checkNonLoadingCache();
//调用LocalCache构造方法,但loader为null
return new LocalCache.LocalManualCache<K1, V1>(this);
} //被CacheBuilder的build方法调用,将其参数传递给LocalCache
LocalCache(
CacheBuilder<? super K, ? super V> builder, @Nullable CacheLoader<? super K, V> loader) {
//默认并发水平是4,即四个Segment(但要注意concurrencyLevel不一定等于Segment个数)
//Segment个数:一个刚刚大于或等于concurrencyLevel且是2的几次方的一个数,在后面会有segmentCount赋值过程
concurrencyLevel = Math.min(builder.getConcurrencyLevel(), MAX_SEGMENTS); keyStrength = builder.getKeyStrength();//默认为Strong,即强引用
valueStrength = builder.getValueStrength();//默认为Strong,即强引用 keyEquivalence = builder.getKeyEquivalence();
valueEquivalence = builder.getValueEquivalence(); maxWeight = builder.getMaximumWeight();
weigher = builder.getWeigher();
expireAfterAccessNanos = builder.getExpireAfterAccessNanos();
expireAfterWriteNanos = builder.getExpireAfterWriteNanos();
refreshNanos = builder.getRefreshNanos(); removalListener = builder.getRemovalListener();
removalNotificationQueue = (removalListener == NullListener.INSTANCE)
? LocalCache.<RemovalNotification<K, V>>discardingQueue()
: new ConcurrentLinkedQueue<RemovalNotification<K, V>>(); ticker = builder.getTicker(recordsTime());
entryFactory = EntryFactory.getFactory(keyStrength, usesAccessEntries(), usesWriteEntries());
globalStatsCounter = builder.getStatsCounterSupplier().get();
defaultLoader = loader; int initialCapacity = Math.min(builder.getInitialCapacity(), MAXIMUM_CAPACITY);
if (evictsBySize() && !customWeigher()) {
initialCapacity = Math.min(initialCapacity, (int) maxWeight);
} //调整segmentCount个数,通过位移实现,所以是2的n次方。
int segmentShift = 0;
int segmentCount = 1;
while (segmentCount < concurrencyLevel
&& (!evictsBySize() || segmentCount * 20 <= maxWeight)) {
++segmentShift;
segmentCount <<= 1;
}
this.segmentShift = 32 - segmentShift;
segmentMask = segmentCount - 1;
//初始化segments
this.segments = newSegmentArray(segmentCount);
//每个segment的大小
int segmentCapacity = initialCapacity / segmentCount;
if (segmentCapacity * segmentCount < initialCapacity) {
++segmentCapacity;
} int segmentSize = 1;
while (segmentSize < segmentCapacity) {
segmentSize <<= 1;
}
//初始化Segments
if (evictsBySize()) {
// Ensure sum of segment max weights = overall max weights
long maxSegmentWeight = maxWeight / segmentCount + 1;
long remainder = maxWeight % segmentCount;
for (int i = 0; i < this.segments.length; ++i) {
if (i == remainder) {
maxSegmentWeight--;
}
this.segments[i] =
createSegment(segmentSize, maxSegmentWeight, builder.getStatsCounterSupplier().get());
}
} else {
for (int i = 0; i < this.segments.length; ++i) {
this.segments[i] =
createSegment(segmentSize, UNSET_INT, builder.getStatsCounterSupplier().get());
}
}
} //Segment初始化操作,结构与上面图中大致相同(图中省去部分队列)
Segment(LocalCache<K, V> map, int initialCapacity, long maxSegmentWeight,
StatsCounter statsCounter) {
this.map = map;
this.maxSegmentWeight = maxSegmentWeight;
this.statsCounter = checkNotNull(statsCounter);
//初始化table
initTable(newEntryArray(initialCapacity));
//key引用队列
keyReferenceQueue = map.usesKeyReferences()
? new ReferenceQueue<K>() : null;
//value引用队列
valueReferenceQueue = map.usesValueReferences()
? new ReferenceQueue<V>() : null; recencyQueue = map.usesAccessQueue()
? new ConcurrentLinkedQueue<ReferenceEntry<K, V>>()
: LocalCache.<ReferenceEntry<K, V>>discardingQueue();
//写入元素队列
writeQueue = map.usesWriteQueue()
? new WriteQueue<K, V>()
: LocalCache.<ReferenceEntry<K, V>>discardingQueue();
//访问元素队列
accessQueue = map.usesAccessQueue()
? new AccessQueue<K, V>()
: LocalCache.<ReferenceEntry<K, V>>discardingQueue();
}

LocalCache

LocalCache是guava cache的核心类。LocalCache的构造函数在上面已经分析过,接着看下核心方法。

对于get(key, loader)方法流程:

  • 对key做hash,找到存储的segment及数组table上的位置;
  • 链表上查找entry,如果entry不为空,且value没有过期,则返回value,并刷新entry。
  • 若链表上找不到entry,或者value已经过期,则调用lockedGetOrLoad。
  • 锁住整个segment,遍历entry可能在的链表,查看数据是否存在是否过期,若存在则返回。若过期则删除(table,各种queue)。若不存在,则新建一个entry插入table。放开整个segment的锁。
  • 锁住entry,调用loader的reload方法,从数据源加载数据,然后调用storeLoadedValue更新缓存。
  • storeLoadedValue时,锁住整个segment,将value设置到entry中,并设置相关数据(入写入/访问队列,加载/命中数据等)。

getAll(keys)方法:

  • 循环调用get方法,从缓存中获取key对应的value。没有命中的记录下来。
  • 如果有没有命中的key,调用loadAll(keys,loader)方法加载数据。
  • 将加载的数据依次缓存,调用segment的put(K key, int hash, V value, boolean onlyIfAbsent)方法。
  • put时,锁住整个segment,将数据插入链表,更新统计数据。

put(key,value)方法:

  • 对key做hash,找到segment的位置和table上的位置;
  • 锁住整个segment,将数据插入链表,更新统计数据。

putAll(map) 循环调用put方法。

putIfAbsent(key, value) 缓存中,键值对不存在的时候才插入。

实践

guava cache是将数据源中的数据缓存在本地,那如果我们想把远端数据源中的数据缓存在远端 分布式缓存(如redis),可以怎么来使用guava cache的方式进行封装呢?

可以仿照guava写一个简单的缓存,定义如下:

CacheBuilder类 : 配置缓存参数,构建缓存。同上面所讲。

Cache接口:定义增删查接口。

MyCache类:实现Cache接口,put -> 存入DB,更新缓存; get -> 查询缓存,存在即返回;若不存在,查询DB,更新缓存,返回。

CacheLoader类:供MyCache调用,get和getAll时提供单次查DB和批量查DB。

参考

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

Google Guava 缓存

后续

guava cache基于引用回收相关;

删除监听器相关。

guava cache使用和源码分析的更多相关文章

  1. Kubernetes Job Controller 原理和源码分析(二)

    概述程序入口Job controller 的创建Controller 对象NewController()podControlEventHandlerJob AddFunc DeleteFuncJob ...

  2. Kubernetes Job Controller 原理和源码分析(三)

    概述Job controller 的启动processNextWorkItem()核心调谐逻辑入口 - syncJob()Pod 数量管理 - manageJob()小结 概述 源码版本:kubern ...

  3. Quartz学习--二 Hello Quartz! 和源码分析

    Quartz学习--二  Hello Quartz! 和源码分析 三.  Hello Quartz! 我会跟着 第一章 6.2 的图来 进行同步代码编写 简单入门示例: 创建一个新的java普通工程 ...

  4. Android Debuggerd 简要介绍和源码分析(转载)

    转载: http://dylangao.com/2014/05/16/android-debuggerd-%E7%AE%80%E8%A6%81%E4%BB%8B%E7%BB%8D%E5%92%8C%E ...

  5. Java并发编程(七)ConcurrentLinkedQueue的实现原理和源码分析

    相关文章 Java并发编程(一)线程定义.状态和属性 Java并发编程(二)同步 Java并发编程(三)volatile域 Java并发编程(四)Java内存模型 Java并发编程(五)Concurr ...

  6. Kubernetes Job Controller 原理和源码分析(一)

    概述什么是 JobJob 入门示例Job 的 specPod Template并发问题其他属性 概述 Job 是主要的 Kubernetes 原生 Workload 资源之一,是在 Kubernete ...

  7. guava eventbus 原理+源码分析

    前言: guava提供的eventbus可以很方便的处理一对多的事件问题, 最近正好使用到了,做个小结,使用的demo网上已经很多了,不再赘述,本文主要是源码分析+使用注意点+新老版本eventbus ...

  8. jQuery静态方法globalEval使用和源码分析

    Eval函数大家都很熟悉,但是globalEval方法却很少使用,大多数参考手册也没有相关api,下面就对其用法和源码相应介绍: jQuery.globalEval()函数用于全局性地执行一段Java ...

  9. Mybatis的缓存——一级缓存和源码分析

    目录 什么是缓存? 一级缓存 测试一. 测试二. 总结: 一级缓存源码分析: 1. 一级缓存到底是什么? 得出结论: 2. 一级缓存什么时候被创建? 3. 一级缓存的执行流程 结论: 一级缓存源码分析 ...

随机推荐

  1. the c programing language 学习过程2

    manipulated 操纵  notations符号 hexadecimal十六进制 precision精度 be concatenated at 把····联系起来 enumerations枚举  ...

  2. 《清华梦的粉碎》by王垠

     清华梦的诞生 小时候,妈妈给我一个梦.她指着一个大哥哥的照片对我说,这是爸爸的学生,他考上了清华大学,他是我们中学的骄傲.长大后,你也要进入清华大学读书,为我们家争光.我不知道清华是什么样子,但是我 ...

  3. nyoj886 取石子(八) 威佐夫博弈

    好累,坐了一天火车, 终于到学校了. 思路:仔细观察威佐夫博弈,发现P态的所有数字都是不重复的,例如(0,0).(1,2).(3,5).(4,7).(6, 10).(8,13).(9,15).(11, ...

  4. 【BZOJ2095】 Bridge

    Time Limit: 1000 ms   Memory Limit: 128 MB Description YYD为了减肥,他来到了瘦海,这是一个巨大的海,海中有n个小岛,小岛之间有m座桥连接,两个 ...

  5. php面试上机题(2018-3-3)

    需求:将第三方api的前3000条数据全部读取出来,存入对应的数据库字段 第三方api:http://pub.cloudmob.mobi/publisherapi/offers/?uid=92& ...

  6. 一句Python,一句R︱数据的合并、分组、排序、翻转

    先学了R,最近刚刚上手python,所以想着将python和R结合起来互相对比来更好理解python.最好就是一句python,对应写一句R. python中的numpy模块相当于R中的matirx矩 ...

  7. Caused by: org.springframework.beans.factory.BeanCreationException: Could not autowire field

    1 错误描述 org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'com.s ...

  8. val(),text(),html()的区别

    <script type="text/javascript" src="js/jquery-1.8.3.min.js"></script> ...

  9. 阿里云ECS+CentOS 7.0+Docker+Redmine环境搭建

    前言 搭建Redmine环境可以选择使用分别安装Ruby+Rails+Redmine+MySQL的方式, 但是过程中需要解决各种扰人的依赖问题.Docker为我们提供的Plan B,方便,快捷. 安装 ...

  10. Docker部署Apollo配置中心

    1.Apollot简述 Apollo(阿波罗)是携程框架部门研发的分布式配置中心,能够集中化管理应用不同环境.不同集群的配置,配置修改后能够实时推送到应用端,并且具备规范的权限.流程治理等特性,适用于 ...