一、添加
Caffeine提供了四种缓存添加策略:手动加载,自动加载,手动异步加载和自动异步加载。
1、手动加载
cache.get(key, k -> value) ,当在缓存中不存在该key对应的缓存元素的时候,进行计算生成并直接写入至缓存内,而当该key对应的缓存元素存在的时候将会直接返回存在的缓存值。当缓存的元素无法生成或者在生成的过程中抛出异常而导致生成元素失败,cache.get 会返回 null
。
cache.put(key, value) 操作将会直接写入或者更新缓存里的缓存元素,在缓存中已经存在的该key对应缓存值都会直接被覆盖。
Cache<String, User> cache = Caffeine.newBuilder().expireAfterWrite(3, TimeUnit.SECONDS).maximumSize(10).build();
// 查找一个缓存元素, 没有查找到的时候返回null
System.out.println( cache.getIfPresent("111"));
// 查找缓存,如果缓存不存在则生成缓存元素, 如果无法生成则返回null
cache.get("111",key->{
return new User("张三",23);
});
// 添加或者更新一个缓存元素
cache.put("222",new User("李四",23));
System.out.println( cache.getIfPresent("111"));
System.out.println( cache.getIfPresent("222"));
输出结果:
null
User(name=张三, age=23)
User(name=李四, age=23)
从输出结果可以看出,执行了cache.get(key, k -> value)方法以后,由于缓存中没有值为"111"的key,会往caffeine缓存中生成一个User(name=张三, age=23)。
2、自动加载
LoadingCache
是Cache
附加上 CacheLoader
能力之后的缓存实现。
LoadingCache<Object, User> cache = Caffeine.newBuilder().expireAfterWrite(3, TimeUnit.SECONDS).maximumSize(10).build(key -> {
return new User("张三", 23);
});
System.out.println(cache.get("111"));
输出结果:
User(name=张三, age=23)
从输出结果可以看出,自动加载是在初始化缓存时,就设置了如果key不存在,如何生成value的策略。当获取值为“111”的key时,会自动按配置生成value。
3、手动异步加载
AsyncCache<String, User> cache = Caffeine.newBuilder().expireAfterWrite(3, TimeUnit.SECONDS).maximumSize(10).buildAsync();
// 查找一个缓存元素, 没有查找到的时候返回null
cache.get("111", key -> {
return new User("张三", 23);
});
CompletableFuture<User> user =new CompletableFuture<>();
user.complete(new User("李四",23));
添加或者更新一个缓存元素
cache.put("222",user);
System.out.println( cache.getIfPresent("111").get());
System.out.println( cache.getIfPresent("222").get());
输出结果:
User(name=张三, age=23)
User(name=李四, age=23)
AsyncCache 是 Cache 的一个变体,AsyncCache提供了在 Executor上生成缓存元素并返回 CompletableFuture的能力。
默认的线程池实现是 ForkJoinPool.commonPool() ,当然你也可以通过覆盖并实现 方法来自定义你的线程池选择。
4、自动异步加载
AsyncLoadingCache<Object, User> cache = Caffeine.newBuilder().expireAfterWrite(3, TimeUnit.SECONDS).maximumSize(10).buildAsync(key -> {
return new User("张三", 23);
});
System.out.println( cache.get("111").get());
输出结果:
User(name=张三, age=23)
从输出结果可以看出,自动加载是在初始化缓存时,就设置了如果key不存在,如何生成value的策略。当获取值为“111”的key时,会自动按配置生成value。
二、移除
- 驱逐 缓存元素因为策略被移除
- 失效 缓存元素被手动移除
- 移除 由于驱逐或者失效而最终导致的结果
1、显示移除
- 单个移除:cache.invalidate(key)
- 批量移除:cache.invalidateAll(keys)
- 全部移除:cache.invalidateAll()
Cache<String, User> cache = Caffeine.newBuilder().expireAfterWrite(3, TimeUnit.SECONDS).maximumSize(10).build();
cache.get("111", key -> {
return new User("张三", 23);
});
System.out.println(cache.getIfPresent("111"));
//同步缓存移除方式
cache.invalidate("111");
System.out.println(cache.getIfPresent("111"));
AsyncCache<String, User> asyncCache = Caffeine.newBuilder().expireAfterWrite(3, TimeUnit.SECONDS).maximumSize(10).buildAsync();
asyncCache.get("111", key -> {
return new User("张三", 23);
});
System.out.println(asyncCache.getIfPresent("111").get());
//异步缓存移除方式
asyncCache.synchronous().invalidate("111");
System.out.println(asyncCache.getIfPresent("111"));
输出结果:
User(name=张三, age=23)
null
User(name=张三, age=23)
null
2、移除监听器
Caffeine.removalListener(RemovalListener)
方法定义一个移除监听器在一个元素被移除的时候进行相应的操作。这些操作是使用 Executor 异步执行的,其中默认的 Executor 实现是 ForkJoinPool.commonPool() 并且可以通过覆盖Caffeine.executor(Executor)
方法自定义线程池的实现。
当移除之后的自定义操作必须要同步执行的时候,你需要使用 Caffeine.evictionListener(RemovalListener)
。这个监听器将在 RemovalCause.wasEvicted()
为 true 的时候被触发。为了移除操作能够明确生效, Cache.asMap()
提供了方法来执行原子操作。
任何在 RemovalListener
中被抛出的异常将会被打印日志 (通过Logger)并被吞食。
Cache<String, User> cache = Caffeine.newBuilder().expireAfterWrite(500, TimeUnit.MILLISECONDS).maximumSize(10)
.removalListener((key, value, cause) -> {
System.out.println("asyn key:" + key + " reson:" + cause);
}).build();
for (int i = 0; i < 15; i++) {
cache.get(i + "", key -> {
return new User(key + "", 23);
});
}
cache.put(8 + "", new User(8 + "", 23));
cache.invalidate("5");
cache.invalidate("6");
输出结果:
asyn key:8 reson:REPLACED
asyn key:0 reson:SIZE
asyn key:1 reson:SIZE
asyn key:2 reson:SIZE
asyn key:3 reson:SIZE
asyn key:4 reson:SIZE
asyn key:6 reson:EXPLICIT
asyn key:5 reson:EXPLICIT
三、驱逐策略
Caffeine 提供了三种驱逐策略,分别是基于容量,基于时间和基于引用三种类型。
1、基于容量
(1)基于个数
Caffeine.maximumSize(long)设置缓存中的最大个数。
Cache<String, User> cache = Caffeine.newBuilder().maximumSize(10)
.removalListener((key, value, cause) -> {
System.out.println("asyn key:" + key + " reson:" + cause);
}).build();
for (int i = 0; i < 15; i++) {
cache.get(i + "", key -> {
return new User(key + "", 23);
});
}
输出结果:
asyn key:0 reson:SIZE
asyn key:2 reson:SIZE
asyn key:4 reson:SIZE
asyn key:1 reson:SIZE
asyn key:3 reson:SIZE
生成容器时,设置了最大大小为 10,往里面插入15调数据,通过监听器打印可以看出,从第11个数据开始插入后,缓存中的原本数据开始被驱逐。
(2)基于权重
Caffeine.weigher(Weigher):设置每个元素的权重
Caffeine.maximumWeight(long):设置最大权重
基于权重驱逐的策略下,一个缓存元素的权重计算是在其创建和更新时,此后其权重值都是静态存在的,在两个元素之间进行权重的比较的时候,并不会根据进行相对权重的比较。
Cache<String, User> cache = Caffeine.newBuilder()
.maximumWeight(100)
.weigher((String key,User value) -> {
return value.getAge();
})
.removalListener((key, value, cause) -> {
System.out.println("asyn key:" + key + " reson:" + cause);
}).build();
for (int i = 0; i < 6; i++) {
cache.get(i + "", key -> {
return new User(key + "", 23);
});
}
输出结果:
asyn key:1 reson:SIZE
asyn key:0 reson:SIZE
当插入第5个元素,权重已经为23×5=115,大于最大权重为100,开始驱逐内存中的元素 。
2、基于时间
Caffeine提供了三种方法进行基于时间的驱逐:
expireAfterAccess(long, TimeUnit):
一个元素在上一次读写操作后一段时间之后,在指定的时间后没有被再次访问将会被认定为过期项。在当被缓存的元素时被绑定在一个session上时,当session因为不活跃而使元素过期的情况下,这是理想的选择。expireAfterWrite(long, TimeUnit):
一个元素将会在其创建或者最近一次被更新之后的一段时间后被认定为过期项。在对被缓存的元素的时效性存在要求的场景下,这是理想的选择。expireAfter(Expiry):
一个元素将会在指定的时间后被认定为过期项。当被缓存的元素过期时间收到外部资源影响的时候,这是理想的选择。
(1)expireAfterAccess(long, TimeUnit)
Cache<String, User> cache = Caffeine.newBuilder()
.expireAfterAccess(2,TimeUnit.SECONDS)
.removalListener((key, value, cause) -> {
System.out.println("asyn key:" + key + " reson:" + cause);
}).build();
for (int i = 0; i < 6; i++) {
cache.get(i + "", key -> {
return new User(key + "", 23);
});
}
Thread.sleep(5000);
System.out.println(cache.getIfPresent("1"));
输出结果:
null
asyn key:0 reson:EXPIRED
asyn key:1 reson:EXPIRED
asyn key:4 reson:EXPIRED
asyn key:5 reson:EXPIRED
asyn key:3 reson:EXPIRED
asyn key:2 reson:EXPIRED
(2) expireAfterWrite(long, TimeUnit)
Cache<String, User> cache = Caffeine.newBuilder()
.expireAfterWrite(4,TimeUnit.SECONDS)
.removalListener((key, value, cause) -> {
System.out.println("asyn key:" + key + " reson:" + cause);
}).build();
cache.put("1", new User("1", 23));
Thread.sleep(5000);
System.out.println(cache.getIfPresent("1"));
(3) expireAfter(Expiry)
Cache<String, User> cache = Caffeine.newBuilder()
.expireAfter(new Expiry<String, User>() {
@Override
//创建后,设置过期时间
public long expireAfterCreate(@NonNull String key, @NonNull User value, long currentTime) {
return 10000;
}
@Override
//更新后,更改过期时间
public long expireAfterUpdate(@NonNull String key, @NonNull User value, long currentTime, @NonNegative long currentDuration) {
return currentDuration;//保持现有的时间
}
@Override
//读取后,更改过期时间
public long expireAfterRead(@NonNull String key, @NonNull User value, long currentTime, @NonNegative long currentDuration) {
return currentDuration;
}
})
.removalListener((key, value, cause) -> {
System.out.println("asyn key:" + key + " reson:" + cause);
}).build();
3、基于引用
在说明基于引用的驱逐策略之前,我们需要先了解一下强引用、软引用、弱引用的概念。
- 强引用:最常见的引用类型,通常在代码中直接使用,如
Object obj = new Object()
所示。这种引用保证了对象不会被垃圾回收器回收。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。 - 软引用:用于实现缓存等场景,它比强引用弱一些。软引用的对象只有在内存不足时才会被回收。这允许应用程序在内存充足时保持对象存活,以便于高效使用缓存。
- 弱引用:弱引用的对象具有更短的生命周期,无论内存是否充足,垃圾回收器都会回收这些对象。弱引用主要用于那些不需要长期存活的对象,如临时数据结构。
Caffeine 允许配置缓存去让GC去帮助清理缓存当中的元素,其中key支持弱引用,而value则支持弱引用和软引用。记住 AsyncCache
不支持软引用和弱引用。
Caffeine.weakKeys()
在保存key的时候将会进行弱引用。这允许在GC的过程中,当key没有被任何强引用指向的时候去将缓存元素回收。由于GC只依赖于引用相等性。这导致在这个情况下,缓存将会通过引用相等(==)而不是对象相等 equals()
去进行key之间的比较。
Caffeine.weakValues()
在保存value的时候将会使用弱引用。这允许在GC的过程中,当value没有被任何强引用指向的时候去将缓存元素回收。由于GC只依赖于引用相等性。这导致在这个情况下,缓存将会通过引用相等(==)而不是对象相等 equals()
去进行value之间的比较。
Caffeine.softValues()
在保存value的时候将会使用软引用。为了相应内存的需要,在GC过程中被软引用的对象将会被通过LRU算法回收。由于使用软引用可能会影响整体性能,我们还是建议通过使用基于缓存容量的驱逐策略代替软引用的使用。同样的,使用 softValues()
将会通过引用相等(==)而不是对象相等 equals()
去进行value之间的比较。
// 当key和缓存元素都不再存在其他强引用的时候驱逐
LoadingCache<String, User> graphs = Caffeine.newBuilder()
.weakKeys()
.weakValues()
.build(key -> {return new User("111",23)});
// 当进行GC的时候进行驱逐
LoadingCache<String, User> graphs = Caffeine.newBuilder()
.softValues()
.build(key -> {return new User("111",23)});