一、引子

缓存有很多种解决方案,常见的是:

1.存储在内存中 : 内存缓存顾名思义直接存储在JVM内存中,JVM宕机那么内存丢失,读写速度快,但受内存大小的限制,且有丢失数据风险。

2.存储在磁盘中: 即从内存落地并序列化写入磁盘的缓存,持久化在磁盘,读写需要IO效率低,但是安全。

3.内存+磁盘组合方式:这种组合模式有很多成熟缓存组件,也是高效且安全的策略,比如redis。

本文分析常用的内存缓存:google cache。源码包:com.google.guava:guava:22.0 jar包下的pcom.google.common.cache包,适用于高并发读写场景,可自定义缓存失效策略。

二、使用方法

2.1 CacheBuilder有3种失效重载模式

1.expireAfterWrite

当 创建 或 写之后的 固定 有效期到达时,数据会被自动从缓存中移除,源码注释如下:

   /**指明每个数据实体:当 创建 或 最新一次更新 之后的 固定值的 有效期到达时,数据会被自动从缓存中移除
* Specifies that each entry should be automatically removed from the cache once a fixed duration
* has elapsed after the entry's creation, or the most recent replacement of its value.
*当间隔被设置为0时,maximumSize设置为0,忽略其它容量和权重的设置。这使得测试时 临时性地 禁用缓存且不用改代码。
* <p>When {@code duration} is zero, this method hands off to {@link #maximumSize(long)
* maximumSize}{@code (0)}, ignoring any otherwise-specified maximum size or weight. This can be
* useful in testing, or to disable caching temporarily without a code change.
*过期的数据实体可能会被Cache.size统计到,但不能进行读写,数据过期后会被清除。
* <p>Expired entries may be counted in {@link Cache#size}, but will never be visible to read or
* write operations. Expired entries are cleaned up as part of the routine maintenance described
* in the class javadoc.
*
* @param duration the length of time after an entry is created that it should be automatically
* removed
* @param unit the unit that {@code duration} is expressed in
* @return this {@code CacheBuilder} instance (for chaining)
* @throws IllegalArgumentException if {@code duration} is negative
* @throws IllegalStateException if the time to live or time to idle was already set
*/
public CacheBuilder<K, V> expireAfterWrite(long duration, TimeUnit unit) {
checkState(
expireAfterWriteNanos == UNSET_INT,
"expireAfterWrite was already set to %s ns",
expireAfterWriteNanos);
checkArgument(duration >= 0, "duration cannot be negative: %s %s", duration, unit);
this.expireAfterWriteNanos = unit.toNanos(duration);
return this;
}

2.expireAfterAccess

指明每个数据实体:当 创建 或 写 或 读 之后的 固定值的有效期到达时,数据会被自动从缓存中移除。读写操作都会重置访问时间,但asMap方法不会。源码注释如下:

 /**指明每个数据实体:当 创建 或 更新 或 访问 之后的 固定值的有效期到达时,数据会被自动从缓存中移除。读写操作都会重置访问时间,但asMap方法不会。
* Specifies that each entry should be automatically removed from the cache once a fixed duration
* has elapsed after the entry's creation, the most recent replacement of its value, or its last
* access. Access time is reset by all cache read and write operations (including
* {@code Cache.asMap().get(Object)} and {@code Cache.asMap().put(K, V)}), but not by operations
* on the collection-views of {@link Cache#asMap}.
* 后面的同expireAfterWrite
* <p>When {@code duration} is zero, this method hands off to {@link #maximumSize(long)
* maximumSize}{@code (0)}, ignoring any otherwise-specified maximum size or weight. This can be
* useful in testing, or to disable caching temporarily without a code change.
*
* <p>Expired entries may be counted in {@link Cache#size}, but will never be visible to read or
* write operations. Expired entries are cleaned up as part of the routine maintenance described
* in the class javadoc.
*
* @param duration the length of time after an entry is last accessed that it should be
* automatically removed
* @param unit the unit that {@code duration} is expressed in
* @return this {@code CacheBuilder} instance (for chaining)
* @throws IllegalArgumentException if {@code duration} is negative
* @throws IllegalStateException if the time to idle or time to live was already set
*/
public CacheBuilder<K, V> expireAfterAccess(long duration, TimeUnit unit) {
checkState(
expireAfterAccessNanos == UNSET_INT,
"expireAfterAccess was already set to %s ns",
expireAfterAccessNanos);
checkArgument(duration >= 0, "duration cannot be negative: %s %s", duration, unit);
this.expireAfterAccessNanos = unit.toNanos(duration);
return this;
}

3.refreshAfterWrite

指明每个数据实体:当 创建 或 写 之后的 固定值的有效期到达时,且新请求过来时,数据会被自动刷新(注意不是删除是异步刷新,不会阻塞读取,先返回旧值,异步重载到数据返回后复写新值)。源码注释如下:

 /**指明每个数据实体:当 创建 或 更新 之后的 固定值的有效期到达时,数据会被自动刷新。刷新方法在LoadingCache接口的refresh()申明,实际最终调用的是CacheLoader的reload()
* Specifies that active entries are eligible for automatic refresh once a fixed duration has
* elapsed after the entry's creation, or the most recent replacement of its value. The semantics
* of refreshes are specified in {@link LoadingCache#refresh}, and are performed by calling
* {@link CacheLoader#reload}.
* 默认reload是同步方法,所以建议用户覆盖reload方法,否则刷新将在无关的读写操作间操作。
* <p>As the default implementation of {@link CacheLoader#reload} is synchronous, it is
* recommended that users of this method override {@link CacheLoader#reload} with an asynchronous
* implementation; otherwise refreshes will be performed during unrelated cache read and write
* operations.
*
* <p>Currently automatic refreshes are performed when the first stale request for an entry
* occurs. The request triggering refresh will make a blocking call to {@link CacheLoader#reload}
* and immediately return the new value if the returned future is complete, and the old value
* otherwise.触发刷新操作的请求会阻塞调用reload方法并且当返回的Future完成时立即返回新值,否则返回旧值。
*
* <p><b>Note:</b> <i>all exceptions thrown during refresh will be logged and then swallowed</i>.
*
* @param duration the length of time after an entry is created that it should be considered
* stale, and thus eligible for refresh
* @param unit the unit that {@code duration} is expressed in
* @return this {@code CacheBuilder} instance (for chaining)
* @throws IllegalArgumentException if {@code duration} is negative
* @throws IllegalStateException if the refresh interval was already set
* @since 11.0
*/
@GwtIncompatible // To be supported (synchronously).
public CacheBuilder<K, V> refreshAfterWrite(long duration, TimeUnit unit) {
checkNotNull(unit);
checkState(refreshNanos == UNSET_INT, "refresh was already set to %s ns", refreshNanos);
checkArgument(duration > 0, "duration must be positive: %s %s", duration, unit);
this.refreshNanos = unit.toNanos(duration);
return this;
}

2.2 测试验证

1)定义一个静态的LoadingCache,用cacheBuilder构造缓存,分别定义了同步load(耗时2秒)和异步reload(耗时2秒)方法。

2)在main方法中,往缓存中设置值,定义3个线程,用CountDownLatch倒计时器模拟3个线程并发读取缓存,最后在主线程分别5秒、0.5秒、2秒时get缓存。

测试代码如下:

 package guava;

 import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors; import java.util.Date;
import java.util.Random;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit; /**
* @ClassName guava.LoadingCacheTest
* @Description 注意refresh并不会主动刷新,而是被检索触发更新value,且随时可返回旧值
* @Author denny
* @Date 2018/4/28 下午12:10
*/
public class LoadingCacheTest { // guava线程池,用来产生ListenableFuture
private static ListeningExecutorService service = MoreExecutors.listeningDecorator(
Executors.newFixedThreadPool(10)); /**
* 1.expireAfterWrite:指定时间内没有创建/覆盖时,会移除该key,下次取的时候触发"同步load"(一个线程执行load)
* 2.refreshAfterWrite:指定时间内没有被创建/覆盖,则指定时间过后,再次访问时,会去刷新该缓存,在新值没有到来之前,始终返回旧值
* "异步reload"(也是一个线程执行reload)
* 3.expireAfterAccess:指定时间内没有读写,会移除该key,下次取的时候从loading中取
* 区别:指定时间过后,expire是remove该key,下次访问是同步去获取返回新值;
* 而refresh则是指定时间后,不会remove该key,下次访问会触发刷新,新值没有回来时返回旧值
*
* 同时使用:可避免定时刷新+定时删除下次访问载入
*/
private static final LoadingCache<String, String> cache = CacheBuilder.newBuilder()
.maximumSize(1000)
//.refreshAfterWrite(1, TimeUnit.SECONDS)
.expireAfterWrite(1, TimeUnit.SECONDS)
//.expireAfterAccess(1,TimeUnit.SECONDS)
.build(new CacheLoader<String, String>() {
@Override
public String load(String key) throws Exception {
System.out.println(Thread.currentThread().getName() +"==load start=="+",时间=" + new Date());
// 模拟同步重载耗时2秒
Thread.sleep(2000);
String value = "load-" + new Random().nextInt(10);
System.out.println(
Thread.currentThread().getName() + "==load end==同步耗时2秒重载数据-key=" + key + ",value="+value+",时间=" + new Date());
return value;
} @Override
public ListenableFuture<String> reload(final String key, final String oldValue)
throws Exception {
System.out.println(
Thread.currentThread().getName() + "==reload ==异步重载-key=" + key + ",时间=" + new Date());
return service.submit(new Callable<String>() {
@Override
public String call() throws Exception {
/* 模拟异步重载耗时2秒 */
Thread.sleep(2000);
String value = "reload-" + new Random().nextInt(10);
System.out.println(Thread.currentThread().getName() + "==reload-callable-result="+value+ ",时间=" + new Date());
return value;
}
});
}
}); //倒计时器
private static CountDownLatch latch = new CountDownLatch(1); public static void main(String[] args) throws Exception { System.out.println("启动-设置缓存" + ",时间=" + new Date());
cache.put("name", "张三");
System.out.println("缓存是否存在=" + cache.getIfPresent("name"));
//休眠
Thread.sleep(2000);
//System.out.println("2秒后"+",时间="+new Date());
System.out.println("2秒后,缓存是否存在=" + cache.getIfPresent("name"));
//启动3个线程
for (int i = 0; i < 3; i++) {
startThread(i);
} // -1直接=0,唤醒所有线程读取缓存,模拟并发访问缓存
latch.countDown();
//模拟串行读缓存
Thread.sleep(5000);
System.out.println(Thread.currentThread().getName() + "休眠5秒后,读缓存="+cache.get("name")+",时间=" + new Date());
Thread.sleep(500);
System.out.println(Thread.currentThread().getName() + "距离上一次读0.5秒后,读缓存="+cache.get("name")+",时间=" + new Date());
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + "距离上一次读2秒后,读缓存="+cache.get("name")+",时间=" + new Date());
} private static void startThread(int id) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName() + "...begin" + ",时间=" + new Date());
//休眠,当倒计时器=0时唤醒线程
latch.await();
//读缓存
System.out.println(
Thread.currentThread().getName() + "并发读缓存=" + cache.get("name") + ",时间=" + new Date());
} catch (Exception e) {
e.printStackTrace();
}
}
}); t.setName("Thread-" + id);
t.start();
}
}

结果分析

1.expireAfterWrite:当 创建 或 写 之后的 有效期到达时,数据会被自动从缓存中移除

启动-设置缓存,时间=Thu May 17 17:55:36 CST 2018-->主线程启动,缓存创建完毕并设值,即触发写缓存
缓存是否存在=张三
2秒后,缓存是否存在=null--》设定了1秒自动删除缓存,2秒后缓存不存在
Thread-0...begin,时间=Thu May 17 17:55:38 CST 2018--》38秒时,启动3个线程模拟并发读:三个线程读缓存,由于缓存不存在,阻塞在get方法上,等待其中一个线程去同步load数据
Thread-1...begin,时间=Thu May 17 17:55:38 CST 2018
Thread-2...begin,时间=Thu May 17 17:55:38 CST 2018
Thread-1==load start==,时间=Thu May 17 17:55:38 CST 2018---线程1,同步载入数据load()
Thread-1==load end==同步耗时2秒重载数据-key=name,value=load-2,时间=Thu May 17 17:55:40 CST 2018--线程1,同步载入数据load()完毕!,即40秒时写入数据:load-2
Thread-0并发读缓存=load-2,时间=Thu May 17 17:55:40 CST 2018---线程1同步载入数据load()完毕后,3个阻塞在get方法的线程得到缓存值:load-2
Thread-1并发读缓存=load-2,时间=Thu May 17 17:55:40 CST 2018
Thread-2并发读缓存=load-2,时间=Thu May 17 17:55:40 CST 2018
main==load start==,时间=Thu May 17 17:55:43 CST 2018---主线程访问缓存不存在,执行load()
main==load end==同步耗时2秒重载数据-key=name,value=load-4,时间=Thu May 17 17:55:45 CST 2018---load()完毕!45秒时写入数据:load-4
main休眠5秒后,读缓存=load-4,时间=Thu May 17 17:55:45 CST 2018---主线程得到缓存:load-4
main距离上一次读0.5秒后,读缓存=load-4,时间=Thu May 17 17:55:45 CST 2018--距离上一次写才0.5秒,数据有效:load-4
main==load start==,时间=Thu May 17 17:55:47 CST 2018-47秒时,距离上一次写45秒,超过了1秒,数据无效,再次load()
main==load end==同步耗时2秒重载数据-key=name,value=load-8,时间=Thu May 17 17:55:49 CST 2018--49秒时load()完毕:load-8
main距离上一次读2秒后,读缓存=load-8,时间=Thu May 17 17:55:49 CST 2018--打印get的缓存结果:load-8

2.expireAfterAccess:当 创建 或 写 或 读 之后的 有效期到达时,数据会被自动从缓存中移除

修改测试代码98、99行:

Thread.sleep(700);
System.out.println(Thread.currentThread().getName() + "距离上一次读0.5秒后,读缓存="+cache.get("name")+",时间=" + new Date());

启动-设置缓存,时间=Thu May 17 18:32:38 CST 2018
缓存是否存在=张三
2秒后,缓存是否存在=null
Thread-0...begin,时间=Thu May 17 18:32:40 CST 2018
Thread-1...begin,时间=Thu May 17 18:32:40 CST 2018
Thread-2...begin,时间=Thu May 17 18:32:40 CST 2018
Thread-2==load start==,时间=Thu May 17 18:32:40 CST 2018
Thread-2==load end==同步耗时2秒重载数据-key=name,value=load-6,时间=Thu May 17 18:32:42 CST 2018
Thread-0并发读缓存=load-6,时间=Thu May 17 18:32:42 CST 2018
Thread-1并发读缓存=load-6,时间=Thu May 17 18:32:42 CST 2018
Thread-2并发读缓存=load-6,时间=Thu May 17 18:32:42 CST 2018
main==load start==,时间=Thu May 17 18:32:45 CST 2018
main==load end==同步耗时2秒重载数据-key=name,value=load-7,时间=Thu May 17 18:32:47 CST 2018----47秒时写
main休眠5秒后,读缓存=load-7,时间=Thu May 17 18:32:47 CST 2018
main距离上一次读0.5秒后,读缓存=load-7,时间=Thu May 17 18:32:48 CST 2018---48秒读
main距离上一次读0.5秒后,读缓存=load-7,时间=Thu May 17 18:32:49 CST 2018--49秒距离上一次写47秒,间距大于2秒,但是没有触发load() ,因为48秒时又读了一次,刷新了缓存有效期

3.refreshAfterWrite:当 创建 或 写 之后的 有效期到达时,数据会被自动刷新(注意不是删除是刷新)。

启动-设置缓存,时间=Thu May 17 18:39:59 CST 2018--》59秒写
缓存是否存在=张三
main==reload ==异步重载-key=name,时间=Thu May 17 18:40:01 CST 2018--》01秒,2秒后距离上次写超过1秒,reload异步重载
2秒后,缓存是否存在=张三--》距离上一次写过了2秒,但是会立即返回缓存
Thread-0...begin,时间=Thu May 17 18:40:01 CST 2018--》01秒3个线程并发访问
Thread-1...begin,时间=Thu May 17 18:40:01 CST 2018
Thread-2...begin,时间=Thu May 17 18:40:01 CST 2018
Thread-2并发读缓存=张三,时间=Thu May 17 18:40:01 CST 2018--》01秒3个线程都立即得到了缓存
Thread-0并发读缓存=张三,时间=Thu May 17 18:40:01 CST 2018
Thread-1并发读缓存=张三,时间=Thu May 17 18:40:01 CST 2018
pool-1-thread-1==reload-callable-result=reload-5,时间=Thu May 17 18:40:03 CST 2018--》01秒时的异步,2秒后也就是03秒时,查询结果:reload-5
main==reload ==异步重载-key=name,时间=Thu May 17 18:40:06 CST 2018--》06秒时,距离上一次写时间超过1秒,reload异步重载
main休眠5秒后,读缓存=reload-5,时间=Thu May 17 18:40:06 CST 2018--》06秒时,reload异步重载,立即返回旧值reload-5
main距离上一次读0.5秒后,读缓存=reload-5,时间=Thu May 17 18:40:07 CST 2018
main距离上一次读0.5秒后,读缓存=reload-5,时间=Thu May 17 18:40:07 CST 2018
pool-1-thread-2==reload-callable-result=reload-4,时间=Thu May 17 18:40:08 CST 2018--》06秒时的异步重载,2秒后也就是08秒,查询结果:reload-4

三、源码剖析

前面一节简单演示了google cache的几种用法,本节细看源码。

3.1 简介

我们就从构造器CacheBuilder的源码注释,来看一下google cache的简单介绍:

 //LoadingCache加载缓存和缓存实例是以下的特性的组合:
1 A builder of LoadingCache and Cache instances having any combination of the following features:
automatic loading of entries into the cache-》把数据实体自动载入到缓存中去-》基本特性
least-recently-used eviction when a maximum size is exceeded-》当缓存到达最大数量时回收最少使用的数据-》限制最大内存,避免内存被占满-》高级特性,赞

google cache源码详解的更多相关文章

  1. Guava Cache源码详解

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

  2. spring事务详解(三)源码详解

    系列目录 spring事务详解(一)初探事务 spring事务详解(二)简单样例 spring事务详解(三)源码详解 spring事务详解(四)测试验证 spring事务详解(五)总结提高 一.引子 ...

  3. [转]Linux内核源码详解--iostat

    Linux内核源码详解——命令篇之iostat 转自:http://www.cnblogs.com/york-hust/p/4846497.html 本文主要分析了Linux的iostat命令的源码, ...

  4. saltstack源码详解一

    目录 初识源码流程 入口 1.grains.items 2.pillar.items 2/3: 是否可以用python脚本实现 总结pillar源码分析: @(python之路)[saltstack源 ...

  5. 源码详解系列(七) ------ 全面讲解logback的使用和源码

    什么是logback logback 用于日志记录,可以将日志输出到控制台.文件.数据库和邮件等,相比其它所有的日志系统,logback 更快并且更小,包含了许多独特并且有用的特性. logback ...

  6. RocketMQ源码详解 | Broker篇 · 其三:CommitLog、索引、消费队列

    概述 上一章中,已经介绍了 Broker 的文件系统的各个层次与部分细节,本章将继续了解在逻辑存储层的三个文件 CommitLog.IndexFile.ConsumerQueue 的一些细节.文章最后 ...

  7. RocketMQ源码详解 | Consumer篇 · 其一:消息的 Pull 和 Push

    概述 当消息被存储后,消费者就会将其消费. 这句话简要的概述了一条消息的最总去向,也引出了本文将讨论的问题: 消息什么时候才对被消费者可见? 是在 page cache 中吗?还是在落盘后?还是像 K ...

  8. Spark Streaming揭秘 Day25 StreamingContext和JobScheduler启动源码详解

    Spark Streaming揭秘 Day25 StreamingContext和JobScheduler启动源码详解 今天主要理一下StreamingContext的启动过程,其中最为重要的就是Jo ...

  9. 条件随机场之CRF++源码详解-预测

    这篇文章主要讲解CRF++实现预测的过程,预测的算法以及代码实现相对来说比较简单,所以这篇文章理解起来也会比上一篇条件随机场训练的内容要容易. 预测 上一篇条件随机场训练的源码详解中,有一个地方并没有 ...

随机推荐

  1. 三、编辑 Update set

    文档目录 开始使用  初始化查询实例: LambdaToSql.SqlClient DB = new LambdaToSql.SqlClient(); 更新单个实体对象,必须有主键Guid var e ...

  2. WPF中使用后台代码来控制TreeView的选择项(SelectedItem)以及展开节点操作

    首先为TreeView控件制作一个Style: <Style x:Key="LibraryTreeViewItemStyle" TargetType="{x:Typ ...

  3. centos下 redmind2.6安装

    1.下载安装redmind有关软件 cd /tmp wget http://cache.ruby-lang.org/pub/ruby/2.1/ruby-2.1.5.tar.gz wget http:/ ...

  4. MyBatis 框架之快速入门程序

    一.使用 IDEA 快速创建 Maven 项目 关于如何快速创建 Maven 项目,这个可以参考下面这篇文章: Maven 项目管理工具基础入门系列(一) 二.快速配置 MyBatis 依赖 jar ...

  5. 排序算法的C语言实现(下 线性时间排序:计数排序与基数排序)

    计数排序 计数排序是一种高效的线性排序. 它通过计算一个集合中元素出现的次数来确定集合如何排序.不同于插入排序.快速排序等基于元素比较的排序,计数排序是不需要进行元素比较的,而且它的运行效率要比效率为 ...

  6. go socket

    https://tonybai.com/2015/11/17/tcp-programming-in-golang/ Golang的主要 设计目标之一就是面向大规模后端服务程序,网络通信这块是服务端 程 ...

  7. springMVC中添加restful 风格

    RESTful架构:是一种设计的风格,并不是标准,只是提供了一组设计原则和约束条件,也是目前比较流行的一种互联网软件架构.它结构清晰.符合标准.易于理解.扩展方便,所以正得到越来越多网站的采用. 关于 ...

  8. Spring Security简明实践及相关国际化处理

    别人的都是最佳实践,因为我目前的设置没有按照参考文档推荐,还是采用DelegatingFilterProxy,所以我只能说简明实践.先贴我的applicationContext-security.xm ...

  9. “Location of the Android SDK has not been set up in the preferences”问题的解决

    方法来源:http://stackoverflow.com/questions/5894929/location-of-the-Android-sdk-has-not-been-setup-in-th ...

  10. python3学习笔记1---引用http://python3-cookbook.readthedocs.io/zh_CN/latest/

    2018-02-28数据结构和算法(1) 1.1解压序列赋值给多个变量: 任何的序列(或者是可迭代对象)可以通过一个简单的赋值语句解压并赋值给多个变量. 唯一的前提就是变量的数量必须跟序列元素的数量是 ...