一、Guava Cache介绍
1、JVM缓存
JVM 缓存,是堆缓存。其实就是创建一些全局容器,比如List、Set、Map等。
这些容器用来做数据存储。
这样做的问题:
不能按照一定的规则淘汰数据,如 LRU,LFU,FIFO 等。
清除数据时的回调通知
并发处理能力差,针对并发可以使用CurrentHashMap,但缓存的其他功能需要自行实现缓存过期处理,缓存数据加载刷新等都需要手工实现
2、Guava Cache
Guava是Google提供的一套Java工具包,而Guava Cache是一套非常完善的本地缓存机制(JVM缓存)。
Guava cache的设计来源于CurrentHashMap,可以按照多种策略来清理存储在其中的缓存值且保持很高的并发读写性能。
二、Guava Cache应用场景
1、本地缓存的应用场景:
- 对性能有非常高的要求
- 不经常变化
- 占用内存不大
- 有访问整个集合的需求
- 数据允许不时时一致
2、Guava Cache 的优势
- 缓存过期和淘汰机制
在GuavaCache中可以设置Key的过期时间,包括访问过期和创建过期
GuavaCache在缓存容量达到指定大小时,采用LRU的方式,将不常使用的键值从Cache中删除 - 并发处理能力
GuavaCache类似CurrentHashMap,是线程安全的。
提供了设置并发级别的api,使得缓存支持并发的写入和读取
采用分离锁机制,分离锁能够减小锁力度,提升并发能力
分离锁是分拆锁定,把一个集合看分成若干partition, 每个partiton一把锁。ConcurrentHashMap就是分了16个区域,这16个区域之间是可以并发的。GuavaCache采用Segment做分区。 - 更新锁定
一般情况下,在缓存中查询某个key,如果不存在,则查源数据,并回填缓存。(Cache Aside Pattern)在高并发下会出现,多次查源并重复回填缓存,可能会造成源的宕机(DB),性能下降
GuavaCache可以在CacheLoader的load方法中加以控制,对同一个key,只让一个请求去读源并回填缓存,其他请求阻塞等待。 - 集成数据源
一般我们在业务中操作缓存,都会操作缓存和数据源两部分GuavaCache的get可以集成数据源,在从缓存中读取不到时可以从数据源中读取数据并回填缓存 - 监控缓存加载/命中情况
统计
三、Guava Cache创建方式
GuavaCache有两种创建方式:
CacheLoader和Callable callback
public class GuavaDemo {
public static void main(String args[]) throws Exception {
LoadingCache<String, Object> cache = CacheBuilder.newBuilder()
// 最大3个 //Cache中存储的对象,写入3秒后过期
.maximumSize(3).expireAfterWrite(3,
//记录命中率 失效通知
TimeUnit.SECONDS).recordStats().removalListener(new
RemovalListener<Object, Object>() {
public void onRemoval(RemovalNotification<Object, Object>
notification) {
System.out.println(notification.getKey() + ":" + notification.getCause());
}
}.build(
new CacheLoader<String, Object>() {
@Override
public String load(String s) throws Exception {
return Constants.hm.get(s);
}
}
);
/*
初始化cache
*/
initCache(cache);
System.out.println(cache.size());
displayCache(cache);
System.out.println("=============================");
Thread.sleep(1000);
System.out.println(cache.getIfPresent("1"));
Thread.sleep(2500);
System.out.println("=============================");
displayCache(cache);
}
public static Object get(String key, LoadingCache cache) throws Exception {
Object value = cache.get(key, new Callable() {
@Override
public Object call() throws Exception {
Object v = Constants.hm.get(key);
//设置回缓存
cache.put(key, v);
return v;
}
});
return value;
}
public static void initCache(LoadingCache cache) throws Exception {
/*
前三条记录
*/
for (int i = 1; i <= 3; i++) {
cache.get(String.valueOf(i));
}
}
/**
*获得当前缓存的记录
@param
cache
*@throws Exception
**/
public static void displayCache(LoadingCache cache) throws Exception {
Iterator its = cache.asMap().entrySet().iterator();
while (its.hasNext()) {
System.out.println(its.next().toString());
}
}
}
1、CacheLoader
在创建cache对象时,采用CacheLoader来获取数据,当缓存不存在时能够自动加载数据到缓存中
LoadingCache<String,String> cache = CacheBuilder.newBuilder()
.maximumSize(3)
.build(
new CacheLoader<String, String>() {
@Override
public String load(String s) throws Exception {
return Constants.hm.get(s);
}
}
);
2、Callable Callback
public static Object get(String key,LoadingCache cache)throws Exception{
Object value=cache.get(key, new Callable() {
@Override
public Object call() throws Exception {
String v= Constants.hm.get(key);
//设置回缓存
cache.put(key,v);
return v;
}
});
return value;
}
三、缓存数据删除
GuavaCache的数据删除分为:被动删除和主动删除
1、被动删除
- 基于数据大小的删除
LoadingCache<String,Object> cache= CacheBuilder.newBuilder()
/*
加附加的功能
*/
//最大个数
.maximumSize(3)
.build(new CacheLoader<String, Object>() {
//读取数据源
@Override
public Object load(String key) throws Exception {
return Constants.hm.get(key);
}
});
//读取缓存中的1的数据 缓存有就读取 没有就返回null
System.out.println(cache.getIfPresent("5"));
//读取4 读源并回写缓存 淘汰一个(LRU+FIFO)
get("4",cache);
规则:LRU+FIFO
访问次数一样少的情况下,FIFO
- 基于过期时间的删除
隔多长时间后没有被访问过的key被删除
//缓存中的数据 如果3秒内没有访问则删除
.maximumSize(3).expireAfterAccess(3, TimeUnit.SECONDS)
。。。。
Thread.sleep(1000);
//访问1 1被访问
cache.getIfPresent("1");
//歇了2.1秒
Thread.sleep(2100);
//最后缓存中会留下1
System.out.println("==================================");
display(cache);
写入多长时间后过期
//等同于expire ttl 缓存中对象的生命周期就是3秒
.maximumSize(3).expireAfterWrite(3, TimeUnit.SECONDS)
.build(new CacheLoader<String, Object>() {
//读取数据源
@Override
public Object load(String key) throws Exception {
return Constants.hm.get(key);
}
});
display(cache);
Thread.sleep(1000);
//访问1
cache.getIfPresent("1");
//歇了2.1秒
Thread.sleep(2100);
System.out.println("==================================");
display(cache);
- 基于引用的删除
可以通过weakKeys和weakValues方法指定Cache只保存对缓存记录key和value的弱引用。这样当没有其他强引用指向key和value时,key和value对象就会被垃圾回收器回收
LoadingCache<String,Object> cache = CacheBuilder.newBuilder()
// 最大3个 值的弱引用
.maximumSize(3).weakValues()
.build();
Object value = new Object();
cache.put("1",value);
value = new Object();//原对象不再有强引用
//强制垃圾回收
System.gc();
System.out.println(cache.getIfPresent("1"));
2、主动删除
- 单独删除
//将key=1 删除
cache.invalidate("1");
- 批量删除
//将key=1和2的删除
cache.invalidateAll(Arrays.asList("1","2"));
- 清空所有数据
//清空缓存
cache.invalidateAll();
四、Guava Cache原理
1、GuavaCache核心原理之数据结构
Guava Cache的数据结构跟ConcurrentHashMap类似,但也不完全一样。最基本的区别是ConcurrentMap会一直保存所有添加的元素,直到显式地移除。
相对地,Guava Cache为了限制内存占用,通常都设定为自动回收元素。其数据结构图如下:
- LocalCache为Guava Cache的核心类,包含一个Segment数组组成
- Segement数组的长度决定了cache的并发数
- 每一个Segment使用了单独的锁,其实每个Segment继承了ReentrantLock,对Segment的写操作需要先拿到锁
- 每个Segment由一个table和5个队列组成
- 5个队列:
ReferenceQueue keyReferenceQueue : 已经被GC,需要内部清理的键引用队列
ReferenceQueue valueReferenceQueue : 已经被GC,需要内部清理的值引用队列
ConcurrentlinkedQueue<ReferenceEntry<k,v>> recencyQueue : LRU队列,当segment上达到临界值发生写操作时该队列会移除数据
Queue<ReferenceEntry<K, V>> writeQueue:写队列,按照写入时间进行排序的元素队列,写入一个元素时会把它加入到队列尾部
Queue<ReferenceEntry<K, V>> accessQueue:访问队列,按照访问时间进行排序的元素队列,访问(包括写入)一个元素时会把它加入到队列尾部 - 1个table:
AtomicReferenceArray<ReferenceEntry<K, V>> table:AtomicReferenceArray可以用原子方式更新其元素的对象引用数组 - ReferenceEntry<k,v>
ReferenceEntry是Guava Cache中对一个键值对节点的抽象,每个ReferenceEntry数组项都是一条ReferenceEntry链。并且一个ReferenceEntry包含key、hash、valueReference、next字段
(单链)
Guava Cache使用ReferenceEntry接口来封装一个键值对,而用ValueReference来封装Value值
2、GuavaCache核心原理之回收机制
Guava Cache提供了三种基本的缓存回收方式:
- 基于容量回收
在缓存项的数目达到限定值之前,采用LRU的回收方式 - 定时回收
expireAfterAccess:缓存项在给定时间内没有被读/写访问,则回收。回收顺序和基于大小回收一样(LRU)
expireAfterWrite:缓存项在给定时间内没有被写访问(创建或覆盖),则回收 - 基于引用回收
通过使用弱引用的键、或弱引用的值、或软引用的值,Guava Cache可以垃圾回收
除了以上三种还有主动删除,采用命令,上面已写过
GuavaCache构建的缓存不会"自动"执行清理和回收工作,也不会在某个缓存项过期后马上清理,也没有诸如此类的清理机制。
GuavaCache是在每次进行缓存操作的时候,惰性删除 如get()或者put()的时候,判断缓存是否过期
3、GuavaCache核心原理之Segment定位
先通过key做hash定位到所在的Segment
通过位运算找首地址的偏移量 SegmentCount>=并发数且为2的n次方
V get(K key, CacheLoader<? super K, V> loader) throws ExecutionException {
// 注意,key不可为空
int hash = hash(checkNotNull(key));
// 通过hash定位到segment数组的某个Segment元素,然后调用其get方法
return segmentFor(hash).get(key, hash, loader);
}
再找到segment中的Entry链数组,通过key的hash定位到某个Entry节点
V get(K key, int hash, CacheLoader<? super K, V> loader) throws
ExecutionException {
checkNotNull(key);
checkNotNull(loader);
try {
if (count != 0) { // read-volatile
// 内部也是通过找Entry链数组定位到某个Entry节点
ReferenceEntry<K, V> e = getEntry(key, hash);
......