目录
  1. 1. caffeine缓存库
    1. 1.1. caffeine介绍
    2. 1.2. 缓存结构
    3. 1.3. java缓存发展
    4. 1.4. caffeine驱逐策略
      1. 1.4.1. 基于时间驱逐策略
      2. 1.4.2. 基于缓存大小驱逐策略
      3. 1.4.3. 基于引用驱逐策略
    5. 1.5. caffeine缓存策略
      1. 1.5.1. Window TinyLfu缓存策略
    6. 1.6. caffeine动态设置缓存配置
      1. 1.6.1. 动态设置缓存最大值
      2. 1.6.2. 动态设置访问过期时间
      3. 1.6.3. 动态设置写入过期时间
      4. 1.6.4. 动态缓存设置源码分析
    7. 1.7. caffeine加载策略
    8. 1.8. caffeine实战(测试)
      1. 1.8.1. springboot使用caffeine实战
    9. 1.9. 文献参考
caffeine缓存库

caffeine缓存库

caffeine介绍

caffeine是基于guava cache的java缓存库,其api与guava相似。

缓存结构

缓存结构

java缓存发展

java缓存发展

caffeine驱逐策略

基于时间驱逐策略

caffeine为缓存设置过期时间来进行淘汰驱逐。基于时间驱逐策略默认使用jvm内存,jvm内存有多大,就可以缓存多大。

  1. 设置写入时间过期
1
2
3
4
5
6
// 设置写入时间过期
Cache<String , String> cache = Caffeine.newBuilder()
.expireAfterWrite(1 , TimeUnit.SECONDS)
.recordStats() // 获取命中率
.build();
// 解释:数据写入缓存之后的时间,这里设置数据写入缓存1秒钟后过期。
  1. 设置访问时间过期
1
2
3
4
5
6
// 设置访问时间过期
Cache<String , String> cache = Caffeine.newBuilder()
.expireAfterAccess(10 , TimeUnit.SECONDS)
.recordStats() // 获取命中率
.build();
//解释:数据写入缓存后,下一次有相同的数据通过get访问缓存之后的时间,这里设置访问缓存10秒钟后过期。

基于缓存大小驱逐策略

caffeine设置缓存容量大小,超出这个容量则采用Window TinyLfu策略删除缓存。

  1. 设置缓存容量最大值
1
2
3
4
5
6
// 设置缓存容量最大值过期
Cache<String , String> cache = Caffeine.newBuilder()
.maximumSize(200000) // 缓存最大容量,如果缓存量大,需要
.recordStats() // 获取命中率
.build();
// 解释:这里设置缓存最大能容纳200000条数据,如果超出这个限制,就会通过Window TinyLfu策略删除缓存。

基于引用驱逐策略

caffeine通过key的引用强度,使用垃圾回收器对key进行回收。

引用类型 被垃圾回收时间 用途 生存时间
强引用 从来不会 对象的一般状态 JVM停止运行时终止
软引用 在内存不足时 对象缓存 内存不足时终止
弱引用 在垃圾回收时 对象缓存 gc运行后终止
虚引用 Unknown Unknown Unknown
  1. 强引用
    强引用从来不会被垃圾回收,当内存满之后抛出OutOfMemoryError异常,直接退出。
1
2
3
4
5
6
7
8
9
10
11
@Test
void test1() {
ArrayList<byte[]> objects = new ArrayList<>();
try {
while (true) {
objects.add(new byte[1024]);
}
}catch (OutOfMemoryError e) {
e.printStackTrace();
}
}
1
2
3
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
2019-11-28 16:14:14.970 INFO 240500 --- [extShutdownHook] o.s.s.concurrent.ThreadPoolTaskExecutor : Shutting down ExecutorService 'applicationTaskExecutor'
*** java.lang.instrument ASSERTION FAILED ***: "!errorOutstanding" with message can't create name string at JPLISAgent.c line: 807
  1. 软引用
    软引用在内存不足时(虚拟机即将抛出OutOfMemoryError异常),jvm会发起一次gc回收,将堆中只被非强引用的对象回收。如果回收之后虚拟机仍然内存不足,则抛出OutOfMemoryError异常。
1
2
3
4
5
6
7
8
9
10
11
12
@Test
void test2(){
ArrayList<SoftReference<byte[]>> softReferences = new ArrayList<>();
ReferenceQueue<Object> objectReferenceQueue = new ReferenceQueue<>();
try {
while (true) {
softReferences.add(new SoftReference<>(new byte[1024] , objectReferenceQueue));
}
}catch (OutOfMemoryError e) {
e.printStackTrace();
}
}
  1. 弱引用
    和软引用类似,比软引用强度更弱。弱引用对象只能活到下一次jvm执行垃圾回收之前(每一次jvm垃圾回收都会回收那些弱引用对象)。
1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
void test3() {
ArrayList<WeakReference<byte[]>> weakReferences = new ArrayList<>();
ReferenceQueue<Object> objectReferenceQueue = new ReferenceQueue<>();

try {
while (true) {
weakReferences.add(new WeakReference<>(new byte[1024] , objectReferenceQueue));
}
}catch (OutOfMemoryError e) {
e.printStackTrace();
}
}
  1. 虚引用
    一个对象是否被回收和指向它的虚引用没关系,也不能通过虚引用得到其指向的对象(get方法直接返回null)。
    虚引用一般会配合 引用队列(ReferenceQueue)来使用。当某个被虚引用指向的对象被回收时,我们可以在其引用队列中得到这个虚引用的对象作为其所指向的对象被回收的一个通知。
1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
void test4() {
ArrayList<PhantomReference<byte[]>> phantomReferences = new ArrayList<>();
ReferenceQueue<Object> objectReferenceQueue = new ReferenceQueue<>();

try {
while (true) {
phantomReferences.add(new PhantomReference<>(new byte[1024] , objectReferenceQueue));
}
}catch (OutOfMemoryError e) {
e.printStackTrace();
}
}
  1. 基于引用驱逐策略
1
2
3
4
5
6
7
8
9
10
11
// 当key和value都没有引用时驱逐缓存
Cache<String , String> cache = Caffeine.newBuilder()
.weakKeys()
.weakValues()
.build();

// Evict when the garbage collector needs to free memory
// 当垃圾收集器需要释放内存时驱逐
Cache<String , String> cache = Caffeine.newBuilder()
.softValues()
.build();

caffeine缓存策略

Window TinyLfu缓存策略

  1. Window TinyLfu缓存策略 介绍
    TinyLfu策略是结合了LFU和LRU以及其他一些算法的特点,它不是纯粹的LFU算法。
  2. Window TinyLfu缓存策略原理解析
    在TinyLfu中使用Count-Min Sketch(访问最低频次)记录访问频次。
    访问频次
    如图所示,四行表示有四种hash算法,key指向每行中的一个数,分别表示key这个数据在每个hash运算中对应的访问频次,然后取出其中最低访问频次作为最终的记录频次。
    为什么要算四次呢?
    对应场景:hash算法会出现冲突。在只有一个hash算法时,现在有数据A和数据B,它们有可能有hash值是相同的。查询访问记录时,数据A和数据B找到同一个hash值,记录同时+1,这个hash保存的频次为最大的那一个频次记录,最终两个数据查询到的访问频次都是一样的。
    两个数据使用hash算法运算四次,就算有几次值都是一样,只要有一次不一样,这两个数据都是不一样的。最终,得到的频次是这四次hash中的最低频次。
    在caffeine中规定最大访问频次为15,15的二进制为1111,总共四位。每个long型64位,被分为四段,存储四种hash算法,因此一条记录占16位。现在有100条记录缓存,按2的幂次,获得最接近100的数(2^7=128),如果是一次hash,就占128位,四次hash就是128*4位。

caffeine动态设置缓存配置

如果要动态设置参数,这个参数必须已经初始化用build()初始化。

动态设置缓存最大值

1
2
3
4
5
6
7
Cache<String , String> cache = Caffeine.newBuilder()
.maximumSize(200000) // 缓存最大容量,如果缓存量大,需要
.recordStats() // 获取命中率
.build();
cache.policy().eviction().ifPresent(eviction -> eviction.setMaximum(eviction.getMaximum()/2)); // 动态设置缓存最大值
Policy.Eviction<String, String> eviction = cache.policy().eviction().get(); // 动态获取缓存最大值
@NonNegative long maximum = eviction.getMaximum();

动态设置访问过期时间

1
2
3
4
5
6
7
Cache<String , String> cache = Caffeine.newBuilder()
.expireAfterAccess(10 , TimeUnit.SECONDS)
.recordStats() // 获取命中率
.build();
cache.policy().expireAfterAccess().我们可以通过"."运算符获取返回对象中的执行方法eviction()(access -> access.setExpiresAfter(10 , TimeUnit.SECONDS)); // 动态设置访问过期时间
Policy.Expiration<String, String> expiration = cache.policy().expireAfterAccess().get(); // 动态获取访问过期时间
@NonNegative long expiresAfter = expiration.getExpiresAfter(TimeUnit.SECONDS);

动态设置写入过期时间

1
2
3
4
5
6
7
Cache<String , String> cache = Caffeine.newBuilder()
.expireAfterWrite(1 , TimeUnit.SECONDS)
.recordStats() // 获取命中率
.build();
cache.policy().expireAfterWrite().ifPresent(write -> write.setExpiresAfter(10 ,TimeUnit.SECONDS)); // 动态设置写入过期时间
Policy.Expiration<String, String> write = cache.policy().expireAfterWrite().get(); // 动态获取写入过期时间
@NonNegative long writeExpiresAfter = write.getExpiresAfter(TimeUnit.SECONDS);

动态缓存设置源码分析

首先,我们进入Cache类,可以看到Cache类信息。
cache
红色箭头标记的policy方法就是实现cache动态配置的代理方法,其返回值是一个Policy对象。
cache_policy
我们可以看到Policy类信息。
policy
我们可以通过".“运算符获取返回对象中的执行方法eviction()
policy_eviction
eviction()方法的返回值类型是Optional<Eviction<K, V>>,我们可以看到Eviction类信息
eviction
我们可以通过”."运算符获取返回对象中的执行方法ifPresent()。这里,就是整个执行链的最底端。我们可以看出,ifPresent()方法的参数是一个函数(这是java8新特性-函数式编程)。
它要求我们输入一个Consumer<? super T>对象,这个对象是一段执行函数,而这个函数的输入值类型就是T,这个T就是Eviction类型,这段函数就是Eviction类型中的方法。
policy_ifpresent

我们最开始的时候就创建了一个Cache对象,这个对象就是被操作对象,最终的操作者就是Consumer<? super T>类型的执行函数,我们可以通过调用get()方法获取cache对象中的信息,也可以通过set方法设置参数进cache对象。

caffeine加载策略

主要使用的就是手动加载。手动加载、同步加载和异步加载都可以使用动态设置。

  1. 手动加载
    手动加载比较灵活,可以让我们显示的控制缓存的检索,更新和删除。
    手动加载需要我们自己使用put(),get()方法来设置缓存和获取缓存。
1
2
3
4
5
6
7
8
Cache<String , String> cache = Caffeine.newBuilder()
.expireAfterAccess(10 , TimeUnit.SECONDS)
.maximumSize(200000) // 缓存最大容量,如果缓存量大,需要
.recordStats() // 获取命中率
.build();
cache.put("hello" , "world");
cache.getIfPresent("hello"); // 直接通过key获取,如果key不存在,返回null
cache.get("hello" , k -> k) ; // 通过key获取,如果缓存不存在,则通过function得到一个新key设置入缓存
  1. 同步加载
    LoadingCache是使用CacheLoader来构建的缓存的值。
    批量查找可以使用getAll方法。默认情况下,getAll将会对缓存中没有值的key分别调用CacheLoader.load方法来构建缓存的值。我们可以重写CacheLoader.loadAll方法来提高getAll的效率。
    注意:您可以编写一个CacheLoader.loadAll来实现为特别请求的key加载值。例如,如果计算某个组中的任何键的值将为该组中的所有键提供值,则loadAll可能会同时加载该组的其余部分。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
LoadingCache<String, Object> loadingCache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(key -> createExpensiveTest(key));

String key = "test";
// 采用同步方式去获取一个缓存和上面的手动方式是一个原理。在build Cache的时候会提供一个getKey函数。
// 如果查询的时候缓存中没有这个key,createExpensiveTest()将会构建一个新缓存
Object test = loadingCache.get(key);

// 获取组key的值返回一个Map
List<String> keys = new ArrayList<>();
keys.add(key);
Map<String, Object> tests = loadingCache.getAll(keys);

private String getKey(String key){
return key ;
}
  1. 异步加载
    AsyncLoadingCache是继承自LoadingCache类的,异步加载使用Executor去调用方法并返回一个CompletableFuture。异步加载缓存使用了响应式编程模型。
    如果要以同步方式调用时,应提供CacheLoader。要以异步表示时,应该提供一个AsyncCacheLoader,并返回一个CompletableFuture。
    synchronous()这个方法返回了一个LoadingCacheView视图,LoadingCacheView也继承自LoadingCache。调用该方法后就相当于你将一个异步加载的缓存AsyncLoadingCache转换成了一个同步加载的缓存LoadingCache。
    默认使用ForkJoinPool.commonPool()来执行异步线程,但是我们可以通过Caffeine.executor(Executor) 方法来替换线程池。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
AsyncLoadingCache<String, Object> asyncLoadingCache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(10, TimeUnit.MINUTES)
// Either: Build with a synchronous computation that is wrapped as asynchronous
.buildAsync(key -> createExpensiveGraph(key));
// Or: Build with a asynchronous computation that returns a future
// .buildAsync((key, executor) -> createExpensiveTestAsync(key, executor));

String key = "test";

// 查询并在缺失的情况下使用异步的方式来构建缓存
CompletableFuture<Object> test = asyncLoadingCache.get(key);
// 查询一组缓存并在缺失的情况下使用异步的方式来构建缓存
List<String> keys = new ArrayList<>();
keys.add(key);
CompletableFuture<Map<String, Object>> tests = asyncLoadingCache.getAll(keys);
// 异步转同步
loadingCache = asyncLoadingCache.synchronous();

caffeine实战(测试)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
    @Test
void contextLoads1() throws InterruptedException {
String key = "https://www.test.com" ;
String value = "test";

// 基于时间驱逐策略 : 设置缓存时间(写入时间或者访问时间)过期
// 基于缓存大小驱逐策略 : 设置缓存容量最大值,超出最大值使用Window TinyLfu策略删除缓存
Cache<String , String> cache = Caffeine.newBuilder()
.expireAfterAccess(10 , TimeUnit.SECONDS)
// .expireAfterWrite(1 , TimeUnit.SECONDS) // 先注释掉
.maximumSize(200000) // 缓存最大容量,如果缓存量大,需要
.recordStats() // 获取命中率
.build();

cache.policy().eviction().ifPresent(eviction -> eviction.setMaximum(eviction.getMaximum()/2));
Policy.Eviction<String, String> eviction = cache.policy().eviction().get();
@NonNegative long maximum = eviction.getMaximum();

cache.policy().expireAfterAccess().ifPresent(access -> access.setExpiresAfter(10 , TimeUnit.SECONDS));
Policy.Expiration<String, String> expiration = cache.policy().expireAfterAccess().get();
@NonNegative long expiresAfter = expiration.getExpiresAfter(TimeUnit.SECONDS);

// 没有设置expireAfterWrite
// cache.policy().expireAfterWrite().ifPresent(write -> write.setExpiresAfter(10 ,TimeUnit.SECONDS));
// Policy.Expiration<String, String> write = cache.policy().expireAfterWrite().get();
// @NonNegative long writeExpiresAfter = write.getExpiresAfter(TimeUnit.SECONDS);
// System.out.println("max: " + maximum);
System.out.println("duration: " + expiresAfter);
// 获取当前使用内存
Runtime runtime = Runtime.getRuntime();
long start_memory = runtime.totalMemory() - runtime.freeMemory() ;
Long start_time = System.currentTimeMillis() ;
// 设置进缓存,如果设进缓存的量大于最大值,
for (int i=0 ; i<190000 ; i++) {
cache.put(key+i , value);
// cache.get(key+i , s->"error");
}
// 如果超出内存容量,会使用最近不常用算法剔除
for (int i=0 ; i<200000 ; i++) {
cache.getIfPresent(key+i);
}
System.out.println("命中率:%" + cache.stats());

/*cache.policy().eviction().ifPresent(exviction -> exviction.setMaximum(200000));
for (int i=200000 ; i<400000 ; i++) {
cache.put(key+i , value);
// cache.get(key+i , s->"error");
}
// 如果超出内存容量,会使用最近不常用算法剔除
for (int i=200000 ; i<400000 ; i++) {
cache.get(key+i , s -> "error");
}*/
// System.out.println("命中率:%" + cache.stats());
ConcurrentMap<String, String> map = cache.asMap();
int size = map.size();
long end_memory = runtime.totalMemory() - runtime.freeMemory() ;
Long end_time = System.currentTimeMillis();
Double l = (Double.parseDouble(String.valueOf(cache.stats().hitCount()))/Double.parseDouble(String.valueOf((cache.stats().hitCount()+cache.stats().missCount()))))*100;
System.out.println("命中率:%" + l);
System.out.println("缓存执行时间: " + (end_time-start_time) + "ms");
System.out.println("内存使用情况: " + ((end_memory - start_memory)/1024/1024) + "MB");
System.out.println("count: " + size);
}

springboot使用caffeine实战

  1. 首先在pom.xml中添加caffeine依赖
1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/com.github.ben-manes.caffeine/caffeine -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.8.0</version>
</dependency>
  1. 然后创建config配置类CacheConfig.java(这里使用的是手动加载)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.TimeUnit;

@Configuration
public class CacheConfig {

@Bean
public Cache<String , String> cache() {
Cache<String , String> cache = Caffeine.newBuilder()
.expireAfterAccess(10 , TimeUnit.MINUTES)
.maximumSize(2000000) // 缓存最大容量,如果缓存量大,需要
.recordStats() // 获取命中率
.build() ;
return cache ;
}
}
  1. 创建CacheService.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Policy;
import org.checkerframework.checker.index.qual.NonNegative;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

@Service
public class CacheService {
@Autowired
private ConnectorListUpdate connectorListUpdate ;
@Autowired
private Cache<String , String> cache ;

/**
* 设置缓存参数
* @param maxSize
* @param duration
* @return
*/
public Map<String , String> setCache(Long maxSize , Long duration) {
HashMap<String, String> hashMap = new HashMap<>();
// 如果传入的maxSize参数不为空,则修改这个参数
try {
if (maxSize == null && duration == null) {
hashMap.put("code" , "success");
hashMap.put("desc" , "设置的缓存参数为空");
}
if (maxSize != null) {
cache.policy().eviction().ifPresent(eviction -> eviction.setMaximum(maxSize));
}

if (duration != null) {
cache.policy().expireAfterAccess().ifPresent(access -> access.setExpiresAfter(duration , TimeUnit.MINUTES));
}
connectorListUpdate.uriListUpdate();
hashMap.put("code" , success);
hashMap.put("desc" , "缓存参数设置成功");
}catch (Exception e) {
hashMap.put("code" , "faild");
hashMap.put("desc" , "缓存参数设置失败");
}

return hashMap ;
}

/**
* 获取缓存参数
* @return
*/
public Map<String , Object> getCache(){
HashMap<String , Object> data = new HashMap<>();
HashMap<String, Object> map = new HashMap<>();
try {
// 获取最大缓存值
Policy.Eviction<String, String> eviction = cache.policy().eviction().get();
@NonNegative long maximum = eviction.getMaximum();

// 获取时间
Policy.Expiration<String, String> expiration = cache.policy().expireAfterAccess().get();
@NonNegative long duration = expiration.getExpiresAfter(TimeUnit.MINUTES);

// 获取当前缓存条数
long size = cache.estimatedSize() ;

data.put("maxSize" , String.valueOf(maximum));
data.put("duration" , String.valueOf(duration));
data.put("size" , String.valueOf(size));
map.put("code" , "success");
map.put("desc" , "缓存参数获取成功");
map.put("data" , data);
}catch (Exception e) {
map.put("code" , "faild");
map.put("desc" , "缓存参数获取失败");
}

return map ;
}
}
  1. 创建TestController.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.kafka.config.StreamsBuilderFactoryBean;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;

@RestController
@RequestMapping(value = "/test")
public class TestController {
@Autowired
private CacheService cacheService ;

@RequestMapping(value = "/setCache" , method = RequestMethod.GET)
public Map<String , String> setCache(@RequestParam(name = "maxSize") String maxSize ,
@RequestParam(name = "duration") String duration) {
Map<String, String> map = cacheService.setCache(Long.valueOf(maxSize), Long.valueOf(duration));
return map ;
}

@RequestMapping(value = "/getCache" , method = RequestMethod.GET)
public Map<String , Object> getCache(){
Map<String, Object> cache = cacheService.getCache();
return cache ;
}
}

文献参考

  1. 你应该知道的缓存进化史
  2. 深入解密来自未来的缓存-Caffeine
  3. 如何优雅的设计和使用缓存?
  4. Caffeine缓存
  5. 现代化的缓存设计方案
  6. springboot使用caffeine
  7. springboot学习(十二):缓存caffeine的使用
  8. caffeine源码分析——淘汰策略tinylfu
  9. 二分钟快速掌握Caffeine 三种填充策略:手动、同步和异步
  10. 详解 Java 中的四种引用
文章作者: rack-leen
文章链接: http://yoursite.com/2019/11/26/Java/Java%E7%BC%93%E5%AD%98/caffeine%E7%BC%93%E5%AD%98%E5%BA%93/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 rack-leen's blog
打赏
  • 微信
  • 支付宝

评论