guava cache简介
为什么会有guava cache
实际开发中,有时候会有一些不常修改,但是经常会被用到的数据,它们可能放在数据库里,也可能放在配置文件等。每次用到它们,如果都去查数据库,读取配置文件的话,那么效率是比较慢的。我们可以把数据集中放到一个地方,每次查询的时候就去那个地方查。这个地方可以是redis,相比查数据库,redis效率显然会更快,但是还是得跨网络。这个地方也可以是JVM内存,在所有的存放数据的地方中,内存无疑是最快的。存放数据,又是jvm内存,你可能会想到HashMap,如果是比较简单的需求,HashMap已经满足我们的需求了;但是如果你存放数据的同时,又要求可能像redis一样可以设置过期时间,还要求一定要线程安全,这时候HashMap可能就有点力不从心了。而guava cache就是这样一款可以把数据存放到JVM内存,还具有设置过期时间等功能的缓存工具。
guava cache
你可以把 guava cache当做一个可以设定过期机制,多线程安全的map;实际上,guava cache底层的LocalCache也确实是继承了ConcurrentMap。
class LocalCache<K, V> extends AbstractMap<K, V> implements ConcurrentMap<K, V> {
...
}
demo
pom文件
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>23.0</version>
</dependency>
java代码
public static void main(String[] args) throws Exception {
LoadingCache<String, Object> cache = CacheBuilder.newBuilder()
.build(new CacheLoader<String, Object>() {
@Override
public Object load(String key) throws Exception {
return getValue(key);
}
});
System.out.println("key.length = " + cache.get("hello"));
}
private static Object getValue(String key) {
return key.length();
}
利用CacheBuilder.newBuilder().build(new CacheLoader<String, Object>(){...})即可创建一个LocalCache,build里面的new CacheLoader<String, Object>(){...}是根据key加载获取value的方式,我们可以调取rmi加载获取相关信息,也可以从数据库加载获取信息,还可以从配置文件加载获取信息,获取到value以后,LocalCache会把数据保存到内存中,如果第二次获取该key的数据的话,直接从内存中读取数据返回,而不用再次加载了。我这里做演示用,所以只是保存了key的长度。
guava cache配置
加载与插入
guava cache和hashMap最大的不同就是,hashMap想要get某个key对应的value的时候,要先显式把value put进去,但是guava cache有一个加载机制,在创建LocalCache或者get 的时候传入CacheLoader或者Callable实例,每次我们get,guava cache会首先从cache里面查找是否有key对应的value,如果有,返回,如果没有,会调用LocalCache或者callable加载key对应的value,然后隐式的put到cache中并且返回,从而我们不用先显式把value put 进去。
- CacheLoader
public static void main(String[] args) throws Exception {
LoadingCache<String, Object> cache = CacheBuilder.newBuilder()
.build(new CacheLoader<String, Object>() {
@Override
public Object load(String key) throws Exception {
return getValue(key);
}
});
一般我们可以通过cache.get(key)来获取key对应的value,但是这个办法会抛出ExecutionException 的异常,如果我们不想每次都写个try catch来处理ExecutionException ,我们可以用cache.getUnchecked(key),如果用getUnchecked,CacheLoader加载器中不要声明任何检查型异常;批量获取可以用getAll(Iterable<? extends K>)。
- Callable
Cache<String, Object> cache = CacheBuilder.newBuilder().build();
cache.put("hello", "hi");
System.out.println(cache.get("hello", new Callable<Object>() {
@Override
public Object call() throws Exception {
return "hello";
}
}));
我的理解是,这种加载办法是对CacheLoader,或者put的一种补充,如果get找不到对应的value(即返回null),就会调用Callable的回调方法call。比如:
LoadingCache<String, Integer> cache = CacheBuilder.newBuilder()
.build(new CacheLoader<String, Integer>() {
@Override
public Integer load(String key) throws Exception {
return 2;
}
});
//如果上面的load return 2,这里的sout会输出2, 如果上面的load return null,这里的sout会输出1。
System.out.println(cache.get("hello", new Callable<Integer>() {
@Override
public Integer call() throws Exception {
return 1;
}
}));
- put
guava cache也可以像我们普通的map那样显式把key,value put到cache中。
回收
guava cache 和 hashMap第二个不同就是,guava cache有很多种回收机制可以隐式移除某些key,而在hashMap中,我们只能显式的移除key(map.remove(key))。
- 基于容量的回收
CacheBuilder.maximumSize(long)
设置cache的大小,当cache存放的元素超过最大值的时候,最先放入cache的元素会被剔除掉。
LoadingCache<String, Integer> cache = CacheBuilder.newBuilder()
.maximumSize(3)
.build(new CacheLoader<String, Integer>() {
@Override
public Integer load(String key) throws Exception {
System.out.println("--load key =" + key +" --");
return key.length();
}
});
System.out.println(cache.get("a"));
System.out.println(cache.get("a"));
System.out.println(cache.get("b"));
System.out.println(cache.get("c"));
System.out.println(cache.get("d"));
System.out.println(cache.get("a"));
//我们设置了cache最多能存放3个元素,a是最先放进来的,当放完b,c,d以后,再次取a的时候还是需要加载。
- 定时回收
- expireAfterAccess(long, TimeUnit):缓存项在给定时间内没有被读/写访问,则回收。请注意这种缓存的回收顺序和基于大小回收一样。
LoadingCache<String, Integer> cache = CacheBuilder.newBuilder()
.expireAfterAccess(4, TimeUnit.SECONDS)
.build(new CacheLoader<String, Integer>() {
@Override
public Integer load(String key) throws Exception {
System.out.println("--load key =" + key +" --");
return key.length();
}
});
System.out.println(cache.get("a"));
Thread.sleep(3000);
System.out.println(cache.get("a"));
Thread.sleep(3000);
System.out.println(cache.get("a"));
Thread.sleep(4100);
System.out.println(cache.get("a"));
//第一次get会load,第二次,第三次都不会load,第四次因为过了4秒了,所以会load。
- expireAfterWrite(long, TimeUnit):缓存项在给定时间内没有被写访问(创建或覆盖),则回收。如果认为缓存数据总是在固定时候后变得陈旧不可用,这种回收方式是可取的。
LoadingCache<String, Integer> cache = CacheBuilder.newBuilder()
.expireAfterWrite(4, TimeUnit.SECONDS)
.build(new CacheLoader<String, Integer>() {
@Override
public Integer load(String key) throws Exception {
System.out.println("--load key =" + key +" --");
return key.length();
}
});
System.out.println(cache.get("a"));
Thread.sleep(3000);
System.out.println(cache.get("a"));
Thread.sleep(3000);
System.out.println(cache.get("a"));
Thread.sleep(4100);
System.out.println(cache.get("a"));
//第一次get会load,第二次因为还没到4s,所以不会load,第三次会load,因为距离第一次get已经有6秒,超过4秒了,第四次也会load,因为距离第二次get超过了4s。
- 基于引用的回收
通过使用弱引用的键、或弱引用的值、或软引用的值,Guava Cache可以把缓存设置为允许垃圾回收:
- CacheBuilder.weakKeys():使用弱引用存储键。当键没有其它(强或软)引用时,缓存项可以被垃圾回收。因为垃圾回收仅依赖恒等式(==),使用弱引用键的缓存用==而不是equals比较键。
LoadingCache<Cat, Integer> cache = CacheBuilder.newBuilder()
.weakKeys()
.build(new CacheLoader<Cat, Integer>() {
@Override
public Integer load(Cat cat) throws Exception {
System.out.println("--load key =" + cat.getName() + " --");
return cat.getAge();
}
});
// 到最后我们会发现,当cache.size达到4个以后就不会再增加了,这是因为new Cat("cat:" + i, i + 10000)没有被任何东西引用到,每次gc就会被回收掉,如果被gc掉的话,则cache中的key也会没了,所以cache.size才不会源源不断的增长。至于为什么还有4只,我也不太清楚
int i = 1;
while (true) {
cache.get(new Cat("cat:" + i, i + 10000));
System.out.println(cache.size());
System.gc();
Thread.sleep(1000);
}
//如果我们用这段代码,会发现即使有gc,cache.size还是会源源不断的增长,这是因为cat会放到了list中,有被引用到。
int i = 1;
List<Cat> cats = new ArrayList<>();
while (true) {
Cat c = new Cat("cat:" + i, i + 10000);
cats.add(c);
cache.get(c);
System.out.println(cache.size());
System.gc();
Thread.sleep(1000);
}
- CacheBuilder.weakValues():使用弱引用存储值。当值没有其它(强或软)引用时,缓存项可以被垃圾回收。因为垃圾回收仅依赖恒等式(==),使用弱引用值的缓存用==而不是equals比较值。
- CacheBuilder.softValues():使用软引用存储值。软引用只有在响应内存需要时,才按照全局最近最少使用的顺序回收。考虑到使用软引用的性能影响,我们通常建议使用更有性能预测性的缓存大小限定(见上文,基于容量回收)。使用软引用值的缓存同样用==而不是equals比较值。
- 显式清除
- 个别清除:Cache.invalidate(key)
- 批量清除:Cache.invalidateAll(keys)
- 清除所有缓存项:Cache.invalidateAll()
- 移除监听器
我们可以设置移除监听器,当key被移除的时候(invalidate)会触发监听器。
LoadingCache<String, Integer> cache = CacheBuilder.newBuilder()
.removalListener(new RemovalListener<String, Integer>() {
@Override
public void onRemoval(RemovalNotification<String, Integer> removalNotification) {
System.out.println("remove key = " + removalNotification.getKey() + ", value = " + removalNotification.getValue());
}
})
.build(new CacheLoader<String, Integer>() {
@Override
public Integer load(String key) throws Exception {
System.out.println("--load key =" + key + " --");
return key.length();
}
});
cache.get("hello");
cache.invalidate("hello");
- 何时移除
guava cache从来不会主动会回收不符合我们设置回收机制的key-value,即不会开一个线程去回收,比如我们设置了expireAfterAccess(5s),某个键值达到了5s以后,并不会触动监听器,只有当5s以后,我们再次调用该key的时候才会触发该监听器,就算是调用其他key也不会回收该key。
统计
-
recordStats()
开启统计 -
asMap()
获取key value的map
原理
LocalCache的数据结构与ConcurrentHashMap很相似,都由多个segment组成,且各个segment相对独立,互不影响,所以能支持并行操作。每个segment由一个table和若干队列组成。缓存数据存储在table中,其类型为AtomicReferenceArray。