最近突然比较好奇本地内存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, 虚心求教,有错误还请指正轻拍,谢谢
版权声明:本文出自志健的原创文章,未经博主允许不得转载