Guava Cache

缓存,在我们日常开发中是必不可少的一种解决性能问题的方法。简单的说,cache 就是为了提升系统性能而开辟的一块内存空间。

  缓存的主要作用是暂时在内存中保存业务系统的数据处理结果,并且等待下次访问使用。在日常开发的很多场合,由于受限于硬盘IO的性能或者我们自身业务系统的数据处理和获取可能非常费时,当我们发现我们的系统这个数据请求量很大的时候,频繁的IO和频繁的逻辑处理会导致硬盘和CPU资源的瓶颈出现。缓存的作用就是将这些来自不易的数据保存在内存中,当有其他线程或者客户端需要查询相同的数据资源时,直接从缓存的内存块中返回数据,这样不但可以提高系统的响应时间,同时也可以节省对这些数据的处理流程的资源消耗,整体上来说,系统性能会有大大的提升。

  缓存在很多系统和架构中都用广泛的应用,例如:

  1.CPU缓存
  2.操作系统缓存
  3.本地缓存
  4.分布式缓存
  5.HTTP缓存
  6.数据库缓存
  等等,可以说在计算机和网络领域,缓存无处不在。可以这么说,只要有硬件性能不对等,涉及到网络传输的地方都会有缓存的身影。

  Guava Cache是一个全内存的本地缓存实现,它提供了线程安全的实现机制。整体上来说Guava cache 是本地缓存的不二之选,简单易用,性能好。

设想这样一个场景:进行某些热点数据查询时,如果缓存中没有,则去数据库中查询,并把查询到的结果保存到缓存中。 
但假如说数据库中也没有呢? 
这个时候load方法就会抛异常,例如: 

public class Test {

    public static void main(String[] args) {
        //CacheLoader returned null for key 1.
        getGuideInfo(1);
    }

    public static Guide getGuideInfo(Integer guideId) {
        try {
            return testLoadingCache().get(guideId);
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
        return null;
    }

    private static LoadingCache<Integer, Guide> testLoadingCache() throws Exception{
        return CacheBuilder
                .newBuilder()
                .expireAfterAccess(10, TimeUnit.MINUTES)
                .build(new CacheLoader<Integer, Guide>(){
                    @Override
                    public Guide load(Integer guideId) throws Exception {
                        return loadFromDatabase(guideId);
                    }

                });
    }

    /**
     * 假设数据库中不存在
     * @return 从数据库中获取的Perosn对象
     */
    private static Guide loadFromDatabase(Integer guideId) {
        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.  

另一篇

google guava中有cache包,此包提供内存缓存功能。内存缓存需要考虑很多问题,包括并发问题,缓存失效机制,内存不够用时缓存释放,缓存的命中率,缓存的移除等等。 当然这些东西guava都考虑到了。

guava中使用缓存需要先声明一个CacheBuilder对象,并设置缓存的相关参数,然后调用其build方法获得一个Cache接口的实例。请看下面的代码和注释,注意在注释中指定了Cache的各个参数。

public static void main(String[] args) throws ExecutionException, InterruptedException {
    //缓存接口这里是LoadingCache,LoadingCache在缓存项不存在时可以自动加载缓存
    LoadingCache<Integer, Student> studentCache
            //CacheBuilder的构造函数是私有的,只能通过其静态方法newBuilder()来获得CacheBuilder的实例
            = CacheBuilder.newBuilder()
            //设置并发级别为8,并发级别是指可以同时写缓存的线程数
            .concurrencyLevel(8)
            //设置写缓存后8秒钟过期
            .expireAfterWrite(8, TimeUnit.SECONDS)
            //设置缓存容器的初始容量为10
            .initialCapacity(10)
            //设置缓存最大容量为100,超过100之后就会按照LRU最近虽少使用算法来移除缓存项
            .maximumSize(100)
            //设置要统计缓存的命中率
            .recordStats()
            //设置缓存的移除通知
            .removalListener(new RemovalListener<Object, Object>() {
                @Override
                public void onRemoval(RemovalNotification<Object, Object> notification) {
                    System.out.println(notification.getKey() + " was removed, cause is " + notification.getCause());
                }
            })
            //build方法中可以指定CacheLoader,在缓存不存在时通过CacheLoader的实现自动加载缓存
            .build(
                    new CacheLoader<Integer, Student>() {
                        @Override
                        public Student load(Integer key) throws Exception {
                            System.out.println("load student " + key);
                            Student student = new Student();
                            student.setId(key);
                            student.setName("name " + key);
                            return student;
                        }
                    }
            );

    for (int i = 0; i < 20; i++) {
        //从缓存中得到数据,由于我们没有设置过缓存,所以需要通过CacheLoader加载缓存数据
        Student student = studentCache.get(1);
        System.out.println(student);
        //休眠1秒
        TimeUnit.SECONDS.sleep(1);
    }

    System.out.println("cache stats:");
    //最后打印缓存的命中率等 情况
    System.out.println(studentCache.stats().toString());
}

以上程序的输出如下:

load student 1
Student{id=1, name=name 1}
Student{id=1, name=name 1}
Student{id=1, name=name 1}
Student{id=1, name=name 1}
Student{id=1, name=name 1}
Student{id=1, name=name 1}
Student{id=1, name=name 1}
Student{id=1, name=name 1}
1 was removed, cause is EXPIRED
load student 1

......

Student{id=1, name=name 1}
Student{id=1, name=name 1}
Student{id=1, name=name 1}
Student{id=1, name=name 1}
cache stats:
CacheStats{hitCount=17, missCount=3, loadSuccessCount=3, loadExceptionCount=0, totalLoadTime=1348802, evictionCount=2}

看看到在20此循环中命中次数是17次,未命中3次,这是因为我们设定缓存的过期时间是写入后的8秒,所以20秒内会失效两次,另外第一次获取时缓存中也是没有值的,所以才会未命中3次,其他则命中。

guava的内存缓存非常强大,可以设置各种选项,而且很轻量,使用方便。另外还提供了下面一些方法,来方便各种需要:

ImmutableMap<K, V> getAllPresent(Iterable<?> keys) 一次获得多个键的缓存值
put和putAll方法向缓存中添加一个或者多个缓存项
invalidate 和 invalidateAll方法从缓存中移除缓存项
asMap()方法获得缓存数据的ConcurrentMap<K, V>快照
cleanUp()清空缓存
refresh(Key) 刷新缓存,即重新取缓存数据,更新缓存

原文地址:https://www.cnblogs.com/winner-0715/p/8125130.html