最近突然比较好奇本地内存cache该如何实现,guava提供的cache应该是其中的佼佼者,因此花一些时间记录一下所学习到的东西,看看大神们是如何实现这个问题。由于cache里涉及了很多功能,这篇文章只会记录其中我关心的功能。

源码结构

我下载的guava的版本为25.0-jre,包名为com.google.common.cache,这个包里的东西真不少,都是带cache结尾,一时半会有点迷,只是实现一个cache功能,用得着这么多类吗?没办法只能一个一个打开研究它们之间的关系。

下面是几个重要类的说明:

 类名 用途说明
 Cache  接口,里面有不少接口,定义了本地cache所需要大部分接口
LoadingCache  继承Cache,从名称可以看出,它提供了一个刷新的方法,给缓存的定时自动刷新功能提供支持
AbstractCache  抽象类,这个类没有很大的用处,更多的是作为测试的用途,当前的包内未发现有类继承于它,但是它里面定义

了SimpleStatsCounter,这个被作为默认的提供访问统计的功能

AbstractLoadingCache  抽象类,这个类没有很大的用处,更多的是作为测试的用途,和AbstractCache类似
LocalCache  具体实现类,重点需要看的,是主要的功能实现类,里面代码特别长,定义了很多的内部类,其中比较重要的三个类是LocalManualCache,LocalLoadingCache,Segment,ReferenceEntry, 后面会介绍着几个类的主要作用
 CacheBuilder cache的各种参数设置都是通过它来进行,采用了builder模式
 CacheLoader  抽象类,用户可以实现它的load,reload,loadAll方法自定义获取缓存值
 RemovalListener  接口,用户可以实现它的onRemoval方法来获取某一个key被移除的通知
 Weigher  接口,用户可以实现weigh来为每个key定义权重,当cache的总权重大于设置值时,会有换出操作
 CacheStats  描述缓存访问统计的数据,比如hitCount,missCount

 

配置参数

使用cache之前,需要使用CacheBuilder配置各种参数并生成cache对象,一个比较完整的配置参数如下:

LoadingCache<String, String> test = CacheBuilder.newBuilder()
        .maximumSize(100)//cache的最大size
        .expireAfterAccess(10, TimeUnit.MINUTES)//读和写都算access, access多久expire
        .expireAfterWrite(10, TimeUnit.MINUTES)//写后多久expire
        .refreshAfterWrite(10, TimeUnit.MINUTES)//写后多久刷新
        .concurrencyLevel(4)// 配置同时可以有多少个写线程同时写
        .removalListener(notification -> { // 配置监听器
                System.out.println(
                        notification.getKey() + " was removed, cause is " + notification.getCause());
        })
        .build(CacheLoader.from((key)->{return "123";})); // 传入一个CacheLoader

上面的这个是我们经常用的LoadingCache, 如果你不需要自动刷新功能,在build方法的参数里不要传入CacheLoader就可以,这个时候生成的就是LocalManualCache:

Cache<String, String> test2 = CacheBuilder.newBuilder()
        .maximumSize(100)
        .expireAfterAccess(10, TimeUnit.MINUTES)
        .concurrencyLevel(4)
        .removalListener(notification -> {
            System.out.println(
                    notification.getKey() + " was removed, cause is " + notification.getCause());
        })
        .build();

这里需要解释一下LocalManualCache和LocalLoadingCache的区别了:

 LocalManualCache  实现了上面的Cache接口的,可以看出一个简单的内存Map
 LocalLoadingCache  继承LocalManualCache同时实现了上面的LoadingCache接口,实现了refresh方法,具备刷新缓存值的功能

Cache实现过程

以最常用的LocalLoadingCache为例,分为以下几个步骤:

初始化

LocalLoadingCache(
    CacheBuilder<? super K, ? super V> builder, CacheLoader<? super K, V> loader) {
  super(new LocalCache<K, V>(builder, checkNotNull(loader)));
}

从上面的构造函数就能看出,它接受CacheBuilder对象和CacheLoader对象,从CacheBuilder种把我们配置的各项参数读取出来,并开始初始化,截取一小部分代码:

keyStrength = builder.getKeyStrength();
valueStrength = builder.getValueStrength();

keyEquivalence = builder.getKeyEquivalence();
valueEquivalence = builder.getValueEquivalence();

maxWeight = builder.getMaximumWeight();
weigher = builder.getWeigher();
expireAfterAccessNanos = builder.getExpireAfterAccessNanos();
expireAfterWriteNanos = builder.getExpireAfterWriteNanos();
refreshNanos = builder.getRefreshNanos();

现在你只需要知道它根据builder的参数决定cache的初始状态,比如cache的大小,失效时间,配置RemovalListener等等工作。

Cache的组成部分

大部分程序员对ConcurrentMap的实现原理比较熟悉,guava里也有很多类似的概念,其中就有分段Segment,guava的cache也是线程安全的,同样采用了分段的方式,每个Segment中存有一个ReferenceEntry的数组,ReferenceEntry中存了缓存的key,value, 并记录了accessTime和writeTime,这就为后面判断是否expire提供了数据支持。

获取缓存value

当你从缓存中取出某一个key的value时,有两种选择:

1. test.get("kdk");
2. test.get("kdk", new Callable<String>(){
    @Override
    public String call() throws Exception {
        return "DDDD";
    }
});

第一种和普通map取值一样,第二种提供了Callable对象,它代表如果key中的值如果不存在时,会执行Callable获取值返回并存储起来。这个过程中其实还有其他的操作,例如判断是否key expire,更新访问记录状态等等。需要提醒的是如果提供了Callable对象,那你在CacheBuilder中定义的CacheLoader的方法不会被调用。

添加缓存

如果一切正常,你定义的CacheLoader对象总是能在某一个key不存在时帮你把正确的key/value放入缓存中, 当然你如果想手动添加也是可以的,和对map的操作一样,是不是很简单?

test.put("3","12");

缓存满了

根据之前builder定义的maximumSize,会基于LRU缓存回收算法淘汰key/value

结果统计

如果实际项目的使用中各个参数应该设置成多少合适,你可以开启guava cache 的Statistics开关如下:

CacheBuilder.newBuilder().recordStats()

在你做性能调优的时候,缓存运行一段时间后,可以通过stats()方法获取一个CacheStats对象,这个对象中就包含了缓存的实际表现情况:

private final long hitCount;  // 缓存命中数
private final long missCount; // 缓存未命中数
private final long loadSuccessCount; //load成功数
private final long loadExceptionCount; // load异常数
private final long totalLoadTime; //load的总共耗时
private final long evictionCount; //缓存被移出的梳理

这个CacheStats目前我还没有过多使用,未来调优再来更新。

 

 

欢迎关注我的个人的博客www.zhijianliu.cn, 虚心求教,有错误还请指正轻拍,谢谢

版权声明:本文出自志健的原创文章,未经博主允许不得转载

发表评论

电子邮件地址不会被公开。