工作中遇到guava本地cache load方法获取到null导致应用报异常,记录一下。
以下是转载:
1.Guava Cache的get/getIfPresent方法当参数为null时会抛空指针异常
我刚开始使用时还以为Guava Cache跟HashMap一样,get(null)返回null。
实际上Guava整体设计思想就是拒绝null的,很多地方都会执行com.google.common.base.Preconditions.checkNotNull的检查。
2.Guava Cache的load方法不能返回null,否则抛异常
Guava Cache的get方法先在本地缓存中取,如果不存在,则会触发load方法。但load方法不能返回null。
设想这样一个场景:进行某些热点数据查询时,如果缓存中没有,则去数据库中查询,并把查询到的结果保存到缓存中。
但假如说数据库中也没有呢?
这个时候load方法就会抛异常,例如:
- public enum TestGuavaCache {
- INSTANCE;
- private LoadingCache<String, Person> infos;
- TestGuavaCache() {
- infos = CacheBuilder.newBuilder().expireAfterAccess(10, TimeUnit.MINUTES).build(new CacheLoader<String, Person>() {
- public Person load(String key) throws Exception {
- //load from database
- Person p = loadFromDatabase();
- return p;
- }
- });
- }
- //假设数据库中不存在
- private Person loadFromDatabase() {
- return null;
- }
- public Person get(String key) {
- try {
- return infos.get(key);
- } catch (Exception e) {
- //log exception
- }
- return null;
- }
- }
这是因为Guava Cache认为cache null是无意义的,因此Guava Cache的javadoc里加粗说明: must not be null 。
现实世界没那么理想,肯定会有null的情况,那怎么处理呢?我的处理一般是对Guava Cache的get方法做try-catch。
有时候cache null也是有意义的,例如对于一个key,假如数据库中也没有对应的value,那就把这个情况记录下来,
避免频繁的查询数据库(例如一些攻击性行为),直接在缓存中就把这个key挡住了。
怎么做呢?举例:
- @Test
- public void whenNullValue_thenOptional() {
- CacheLoader<String, Optional<String>> loader;
- loader = new CacheLoader<String, Optional<String>>() {
- @Override
- public Optional<String> load(String key) {
- return Optional.fromNullable(getSuffix(key));
- }
- };
- LoadingCache<String, Optional<String>> cache;
- cache = CacheBuilder.newBuilder().build(loader);
- assertEquals("txt", cache.getUnchecked("text.txt").get());
- assertFalse(cache.getUnchecked("hello").isPresent());
- }
- private String getSuffix(final String str) {
- int lastIndex = str.lastIndexOf('.');
- if (lastIndex == -1) {
- return null;
- }
- return str.substring(lastIndex + 1);
- }
3.什么时候用get,什么时候用getUnchecked
官网文档说:
- . If you have defined a CacheLoader that does not declare any checked exceptions then you can perform cache lookups using getUnchecked(K);
- however care must be taken not to call getUnchecked on caches whose CacheLoaders declare checked exceptions.
字面意思是,如果你的CacheLoader没有定义任何checked Exception,那你可以使用getUnchecked。
这一段话我也不是很理解。。官网上给了一个例子是,load方法没有声明throws Exception,那就可以使用getUnchecked:
- LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
- .expireAfterAccess(10, TimeUnit.MINUTES)
- .build(
- new CacheLoader<Key, Graph>() {
- public Graph load(Key key) { // no checked exception
- return createExpensiveGraph(key);
- }
- });
- ...
- return graphs.getUnchecked(key);
4.如何定义一个普通的Guava Cache,不需要用到load方法
假如只是简单的把Guava Cache当作HashMap或ConcurrentHashMap的替代品,不需要用到load方法,而是手动地插入,可以这样:
- com.google.common.cache.Cache<String, String> cache = CacheBuilder.newBuilder().expireAfterWrite(10, TimeUnit.MINUTES).build();
注意不能用LoadingCache了。
查找:
cache.getIfPresent("xx");
插入:
cache.put("xx", "xxx");
5.Guava Cache的超时机制不是精确的。
我曾经依赖Guava Cache的超时机制和RemovalListener,以实现类似定时任务的功能;后来发现Guava Cache的超时机制是不精确的,例如你设置cache的缓存时间是30秒,
那它存活31秒、32秒,都是有可能的。
官网说:
- Timed expiration is performed with periodic maintenance during writes and occasionally during reads, as discussed below.
- Caches built with CacheBuilder do not perform cleanup and evict values "automatically," or instantly after a value expires, or anything of the sort.
- Instead, it performs small amounts of maintenance during write operations, or during occasional read operations if writes are rare.