Google Guava之--cache
一、简介
Google Guava包含了Google的Java项目许多依赖的库,如:集合 [collections] 、缓存 [caching] 、原生类型支持 [primitives support] 、并发库 [concurrency libraries] 、通用注解 [common annotations] 、字符串处理 [string processing] 、I/O 等等。本文只介绍其中的缓存部分。
Guava Cache是一种本地缓存实现,支持多种缓存过期策略。性能好,简单易用。缓存在很多场景下都是很有用的。如,通过key获取一个value的花费的时间很多,而且获取的次数不止一次的时候,就应该考虑使用缓存。Guava Cache与ConcurrentMap很相似,但也不完全一样。最基本的区别是ConcurrentMap会一直保存所有添加的元素,直到显式地移除。而Guava Cache为了限制内存占用,通常都设定为自动回收元素。在某些场景下,尽管LoadingCache 不回收元素,它也会自动加载缓存。
Guava Cache适用于以下应用场景:
- 系统的访问速度首要考虑,而内存空间为次要考虑。
- 某些key对于的value会被查询多次。
- 缓存中存放的数据总量不会超出内存的全部大小。
本文例子使用的guava 版本为guava-18.0.jar,下载地址如下:
http://central.maven.org/maven2/com/google/guava/guava/18.0/guava-18.0.jar
二、Cache使用方式
在使用缓存的时候,我们经常使用的场景是:
"获取缓存-如果没有-则计算"[get-if-absent-compute]的原子语义.
具体含义:
- 从缓存中取。
- 缓存中存在该数据,直接返回;
- 缓存中不存在该数据,从数据源中取。
- 数据源中存在该数据,放入缓存,并返回;
- 数据源中不存在该数据,返回空。
1、CacheLoader方式
代码如下:
import lombok.Data; @Data public class Person { private String id; private String age; public Person(String id, String age) { this.id = id; this.age = age; } }
import com.gl.springbootapi.Person; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.google.common.collect.Lists; import java.util.List; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; public class TestGuavaCache { static LoadingCache<String, Person> cache; // 模拟数据 static final List<Person> list = Lists.newArrayList( new Person("1", "zhangsan"), new Person("2", "wangwu")); public static void testUserCacheLoader() throws ExecutionException { // 创建cache cache = CacheBuilder.newBuilder()// .refreshAfterWrite(1, TimeUnit.MINUTES)// 给定时间内没有被读/写访问,则回收。 // .expireAfterWrite(5, TimeUnit.SECONDS)//给定时间内没有写访问,则回收。 // .expireAfterAccess(3, TimeUnit.SECONDS)// 缓存过期时间为3秒 .maximumSize(100).// 设置缓存个数 build(new CacheLoader<String, Person>() { /** 当本地缓存命没有中时,调用load方法获取结果并将结果缓存 */ @Override public Person load(String key) throws ExecutionException { System.out.println(key + " load in cache"); return getPerson(key); } // 此时一般我们会进行相关处理,如到数据库去查询 private Person getPerson(String key) throws ExecutionException { System.out.println("getting from DB, please wait..."); for (Person p : list) { if (p.getId().equals(key)) return p; } return null; } }); } public static void main(String[] args) throws ExecutionException { testUserCacheLoader(); for (int i = 0; i < 3; i++) { System.out.println("--- " + i + " ---"); try { System.out.println(cache.get("1")); System.out.println(cache.get("2")); } catch (ExecutionException e) { e.printStackTrace(); } } } }
执行结果如下:
--- 0 --- 1 load in cache getting from DB, please wait... Person(id=1, age=zhangsan) 2 load in cache getting from DB, please wait... Person(id=2, age=wangwu) --- 1 --- Person(id=1, age=zhangsan) Person(id=2, age=wangwu) --- 2 --- Person(id=1, age=zhangsan) Person(id=2, age=wangwu)
第0次从db中获取,第1和2次获取的时候没有执行获取的方法,而是直接从缓存中获取。
2、Callback方式
代码如下:
import java.util.ArrayList; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import com.gl.springbootapi.Person; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.common.collect.Lists; public class TestGuavaCache2 { static Cache<String, Person> cache2 = CacheBuilder.newBuilder().maximumSize(1000).build(); // 模拟数据 static final List<Person> list = Lists.newArrayList( new Person("1", "zhangsan"), new Person("2", "wangwu")); static final String key = "1"; public static void testUserCallback() throws ExecutionException { /** * 用缓存中的get方法,当缓存命中时直接返回结果;否则,通过给定的Callable类call方法获取结果并将结果缓存。<br/> * 可以用一个cache对象缓存多种不同的数据,只需创建不同的Callable对象即可。 */ cache2.get(key, new Callable<Person>() { public Person call() throws ExecutionException { System.out.println(key + " load in cache"); return getPerson(key); } // 此时一般我们会进行相关处理,如到数据库去查询 private Person getPerson(String key) throws ExecutionException { System.out.println("getting from DB, please wait..."); for (Person p : list) { if (p.getId().equals(key)) return p; } return null; } }); } public static void main(String[] args) throws ExecutionException { testUserCallback(); for (int i = 0; i < 3; i++) { System.out.println("--- " + i + " ---"); System.out.println(cache2.getIfPresent("1")); System.out.println(cache2.getIfPresent("2")); } } }
执行结果如下:
1 load in cache getting from DB, please wait... --- 0 --- Person(id=1, age=zhangsan) null --- 1 --- Person(id=1, age=zhangsan) null --- 2 --- Person(id=1, age=zhangsan) null
第0次从db中获取,第1和2次获取的时候没有执行获取的方法,而是直接从缓存中获取。
3、关于移除监听器
通过CacheBuilder.removalListener(RemovalListener),我们可以声明一个监听器,从而可以在缓存被移除时做一些其他的操作。当缓存被移除时,RemovalListener会获取移除bing通知[RemovalNotification],其中包含移除的key、value和RemovalCause。
示例代码如下:
import com.gl.springbootapi.Person; import com.google.common.cache.*; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; public class TestGuavaCache3 { static LoadingCache<String, Person> cache; public static void testListener() throws ExecutionException { CacheLoader<String, Person> loader = new CacheLoader<String, Person>() { @Override // 当本地缓存命没有中时,调用load方法获取结果并将结果缓存 public Person load(String key) throws ExecutionException { System.out.println(key + " load in cache"); return getPerson(key); } // 此时一般我们会进行相关处理,如到数据库去查询 private Person getPerson(String key) throws ExecutionException { System.out.println("getting from DB, please wait..."); return new Person(key, "zhang" + key); } }; // remove listener RemovalListener<String, Person> removalListener = new RemovalListener<String, Person>() { public void onRemoval(RemovalNotification<String, Person> removal) { System.out.println("cause:" + removal.getCause() + " key:" + removal.getKey() + " value:" + removal.getValue()); } }; cache = CacheBuilder.newBuilder()// .expireAfterWrite(2, TimeUnit.MINUTES).maximumSize(1024).removalListener(removalListener).build(loader); cache.get("1");// 放入缓存 cache.get("1");// 第二次获取(此时从缓存中获取) cache.invalidate("1");// 移除缓存 cache.get("1");// 重新获取 cache.get("1");// 再次获取(此时从缓存中获取) } public static void main(String[] args) throws ExecutionException { testListener(); } }
运行结果如下:
1 load in cache getting from DB, please wait... cause:EXPLICIT key:1 value:Person(id=1, age=zhang1) 1 load in cache getting from DB, please wait...
三、其他相关方法
显式插入:该方法可以直接向缓存中插入值,如果缓存中有相同key则之前的会被覆盖。
cache.put(key, value);
显式清除:我们也可以对缓存进行手动清除。
cache.invalidate(key); //单个清除 cache.invalidateAll(keys); //批量清除 cache.invalidateAll(); //清除所有缓存项
基于时间的移除:
expireAfterAccess(long, TimeUnit); 该键值对最后一次访问后超过指定时间再移除 expireAfterWrite(long, TimeUnit) ;该键值对被创建或值被替换后超过指定时间再移除
基于大小的移除:指如果缓存的对象格式即将到达指定的大小,就会将不常用的键值对从cache中移除。
cacheBuilder.maximumSize(long)
size是指cache中缓存的对象个数。当缓存的个数开始接近size的时候系统就会进行移除的操作
缓存清除执行的时间
使用CacheBuilder构建的缓存不会"自动"执行清理和回收工作,也不会在某个缓存项过期后马上清理,也没有诸如此类的清理机制。它是在写操作时顺带做少量的维护工作(清理);如果写操作太少,读操作的时候也会进行少量维护工作。因为如果要自动地持续清理缓存,就必须有一个线程,这个线程会和用户操作竞争共享锁。在某些环境下线程创建可能受限制,这样CacheBuilder就不可用了。
四、其他说明
一般在实际应用的时候我们是在DAO层本地缓存,
为什么要把Cache对象放在DAO层?
思考一下Cache的用途,能够想到在一个Application中,对同一种对象的缓存只需要一个就够了。比如示例中的poiCache是缓存city对应的poi信息的,那么在这个Application中最好只存在一个poiCache。
实现这样的要求有以下几种方式:
- Spring的Singleton模式(推荐)
比较常见的做法是将Server和DAO都作为单例的bean交给Spring容器管理,而将缓存放在DAO层不但实现了单例,也更合理(数据的缓存策略本身就应是数据访问层的一部分)。 - 静态初始化(不推荐)