一文深入了解史上最强的Java堆内缓存框架Caffeine

时间:2020-03-15 14:18:49   收藏:0   阅读:518

它提供了一个近乎最佳的命中率。从性能上秒杀其他一堆进程内缓存框架,Spring5更是为了它放弃了使用多年的GuavaCache

缓存,在我们的日常开发中用的非常多,是我们应对各种性能问题支持高并发的一大利器。我们熟知的缓存有堆缓存(Ehcache3.x、Guava Cache等)、堆外缓存(Ehcache3.x、MapDB等)、分布式缓存(Redis、 memcached等)等等。今天要上场的主角是Caffeine,它其实是Google基于Java8对GuavaCache的重写升级版本,支持丰富的缓存过期策略,尤其是TinyLfu 淘汰算法,提供了一个近乎最佳的命中率。从性能上(读、写、读/写)也足以秒杀其他一堆进程内缓存框架。Spring5更是直接放弃了使用了多年的Guava,而采用了Caffeine。
技术分享图片(以上数据来自官方读写性能测试结果,更多测试结果详见 https://github.com/ben-manes/caffeine/wiki/Benchmarks)

当然在实际使用中基本会涉及中多个缓存的组合使用,比如二级缓存(Caffeine+Redis)、多级缓存等等,这个以后再讲。接下来我们分【基础实战】、【高阶用法】、【理论概述】三个部分来聊一聊史上最强的Java堆内缓存框架。
(在“码大叔”公众号回复数字136即可获取演示源码及牛逼的TinyLfu论文。论文版权归原作者所有,向大神学习致敬)

====基础实战====

接下来我们通过一些例子来演示Caffeine的基础用法,首先我们通springboot新建一个mds-caffeine-demo的Gradle工程。

一、基础配置

1、添加依赖

需要使用到 spring-boot-starter-cache和caffeine两个包

implementation 'org.springframework.boot:spring-boot-starter-cache'
implementation 'com.github.ben-manes.caffeine:caffeine'

2、在applicationyml文件中添加配置

spring:
   cache:
       type: caffeine

3、添加注解

在启动类上添加@EnableCaching
技术分享图片
就是这么地 so easy,Caffeine就已经集成到我们的项目中来了。

二、实战演示

假设我们数据库中有一张User表,里面有【码大叔和小九九】2条数据
|id| name |birdhtday|
|--|--|--|
|1 | 码大叔 |2012-05-12|
|2 | 小九九 |1999-09-19|

场景1:添加及使用缓存

只需要使用@Cacheable注解即可自动将数据添加到缓存中,后续直接从缓存中读取数据。
value:表示缓存的名称,这个参数value还是比较误导人的,不是缓存的值,所以官方还提供了一种写法:cacheNames。
key:表示缓存的key,可以为空。如果指定需要按照SpEL表达式编写

方法1、将用户对象以ID作为key存放到缓存中。

技术分享图片
我们访问页面:
技术分享图片
第一次:打印了数据库操作的日志
技术分享图片 第二次:没有打印,表示缓存添加成功。

方法2、将满足条件的数据存放到缓存中

@Cacheable有一个参数叫做condition,该条件为true时则放到缓存到。该参数同样需使用SpEL表达式。
技术分享图片
接下来我们分别进行用户1、用户2、用户1、用户2 四次查询。我们看到只打印了3条数据,第二次访问用户1从缓存中读取数据,用户2每次都是从数据库中读取数据,没进入缓存。
技术分享图片
【敲黑板】

我们以expireAfterWrite为例,配置如下,然后不停地访问,我们看到每隔5秒后就自动更新一次缓存。
技术分享图片技术分享图片
【敲黑板】

2020-03-08 13:51:51,144|o.s.boot.SpringApplication|reportFailure|Application run failed
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'cacheManager' defined in class path resource [org/springframework/boot/autoconfigure/cache/CaffeineCacheConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.cache.caffeine.CaffeineCacheManager]: Factory method 'cacheManager' threw exception; nested exception is java.lang.IllegalStateException: refreshAfterWrite requires a LoadingCache
   at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:656)
   at com.qiaojs.mds.MDSApplication.main(MDSApplication.java:16)
Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.cache.caffeine.CaffeineCacheManager]: Factory method 'cacheManager' threw exception; nested exception is java.lang.IllegalStateException: refreshAfterWrite requires a LoadingCache
   ... 19 common frames omitted
Caused by: java.lang.IllegalStateException: refreshAfterWrite requires a LoadingCache
   ... 20 common frames omitted

这需要我们去实现一个CacheLoader,再重启就OK了。

@Bean
public CacheLoader<Object, Object> cacheLoader() {
CacheLoader<Object, Object> cacheLoader = new CacheLoader<Object, Object>() {
  @Override
  public Object load(Object key) throws Exception {
    log.info("load key:{}", key);
    return null;
  }
  @Override
  public Object reload(Object key, Object oldValue) throws Exception {
    log.info("reload key:{},oldValue:{}", key, oldValue);
    return oldValue;
  }
};
return cacheLoader;
}

2、前面也提到了Caffeine在缓存过期时默认只有一个线程去加载数据,配置了refreshAfterWrite后当大量请求过来时,可以确保其他用户快速获取响应。但refreshAfterWrite本身默认刷新也是同步的,也就意味着该调用者的线程还会处于等待状态,如有对于响应要求比较高时,可以改写reaload方法让它也异步去执行。

// 1、定义一个线程
private static ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10));
//2、异步加载
 private static LoadingCache<String, String> cache = CacheBuilder.newBuilder().refreshAfterWrite(1, TimeUnit.SECONDS)
           .build(new CacheLoader<String, String>() {
               ……
               @Override
               public ListenableFuture<String> reload(String key, String oldValue) throws Exception {
                    log.info("......后台线程池异步刷新:" + key);
                   return service.submit(callable);
               }

这样就非常地完美了。

4:公共配置

如果一个类里有很多的缓存方法,可以使用@CacheConfig注解。
技术分享图片

5、制定多个缓存规则

有时候我们可能需要配置多个缓存规则,以用户为例,假设用户名为唯一的,我们既要设置id为缓存的key,也要设置userName作为缓存的key,这个时候就可以用@Caching。当然,更新和删除时也都可以使用,我们先看一下它的定义:
技术分享图片
使用举例:
技术分享图片

6、使用Java类配置

在实际使用中,我们很少使用yml或porperties来配置缓存的一些定义,除非缓存的场景或者规则很少,一般都是使用java类来配置。这个就不做多讲,大家可以直接在码大叔公众号回复136获取演示代码

@Bean(name = "caffeineCacheManager")
@Primary
public CacheManager caffeineCacheManager() {
  SimpleCacheManager cacheManager = new SimpleCacheManager();
  ArrayList<CaffeineCache> caches = new ArrayList<CaffeineCache>();
  //方法1:通过枚举定义
  // for (CacheDefineEnum cacheDefine : CacheDefineEnum.values()) {
  // Caffeine<Object, Object> caffeine = Caffeine.newBuilder();
  // if (-1 != cacheDefine.getTtl()) {
  // caffeine.expireAfterWrite(cacheDefine.getTtl(), cacheDefine.getTimeUnit());
  // }
  // Cache<Object, Object> cache = caffeine.maximumSize(cacheDefine.getMaxSize()).build();
  // caches.add(new CaffeineCache(cacheDefine.name(), cache));
  // }
  //方法二:通过
  caches.add(new CaffeineCache("USER",
  Caffeine.newBuilder()
    .expireAfterAccess(5, TimeUnit.SECONDS)
    .build()));
    cacheManager.setCaches(caches);
  return cacheManager;
}

7、查看缓存信息

在开发过程中,如果需要验证缓存是否生效或者我们的配置是否正确,除了看系统的运行行为,我们还可以直接去查看缓存的信息。

private CacheManager cacheManager;   
@GetMapping("/cache/info")
public Object cacheData(String id) {
  Cache cache = cacheManager.getCache("USER");
  if (null == cache.get(id)) {
    return "cache is null";
  }
  Object obj = cache.get(id).get();
  if (null == obj) {
    return "null obj";
  } else {
    return "Object Info:" + obj.toString();
  }
}

8:统计监控

通过使用Caffeine.recordStats(),可以转化成一个统计的集合. 通过 Cache.stats() 返回一个CacheStats。CacheStats提供以下统计方法

注意:

====理论概述====

1、驱逐策略(Eviction)

参考:

https://github.com/ben-manes/caffeine(官方)
https://www.jianshu.com/p/d3bca89b56f7
https://segmentfault.com/a/1190000016091569?utm_source=tag-newest

推荐阅读:
SpringCloud第二代实战系列(一):使用Nacos实现服务注册与发现

感谢各位大佬关注公众号“码大叔”,我们一起交流学习!
微信公众号:码大叔 十年戎“码”,老“叔”开花
技术分享图片

原文:https://www.cnblogs.com/madashu/p/12497025.html

评论(0
© 2014 bubuko.com 版权所有 - 联系我们:wmxa8@hotmail.com
打开技术之扣,分享程序人生!