前言

Caffeine被称为目前JVM缓存性能最好的缓存,本篇讲述的就是JVM缓存的另一种实现。Caffeine是对Guava缓存的升级版(如果需要了解Guava Cache可参考上篇👉学习JVM缓存之GuavaCache),就和Mybatis与Mybatis Plus一样。Caffeine也是Spring5.x后使用的缓存框架,作为Spring推荐的缓存框架我们有必要了解一下。

caffeine读性能测试图

图1

caffeine写性能测试图

图2

图1是Caffeine官方提供的8个线程进行读操作的测试结果,图2是8线程写操作的测试结果。对此还有其他的测试结果,感兴趣的可以去官方查看。

性能测试:https://github.com/ben-manes/caffeine/wiki/Benchmarks

效率测试:https://github.com/ben-manes/caffeine/wiki/Efficiency

本文要讲的是怎么使用这个缓存,至于它是具体怎么实现的、以及使用的算法等等,暂不研究。

简单应用

首先需要引入依赖

  • Maven
<dependency>
  <groupId>com.github.ben-manes.caffeine</groupId>
  <artifactId>caffeine</artifactId>
  <version>2.8.8</version>
</dependency>
  • Gradle
implementation 'com.github.ben-manes.caffeine:caffeine:2.8.8'

如果你用过Guava Cache的话,那么你可以用Guava Cache的方式使用Caffeine,基本上是一样的。

@Test
public void simpleTest() throws InterruptedException {
  LoadingCache<String , Object> cache = Caffeine.newBuilder()
    //最大个数限制(超过最大容量,则移除最长时间不使用的缓存)
    .maximumSize(256L)
    // 缓存最大权重(与maximumSize不兼容)
    // .maximumWeight(100)
    //初始化容量
    .initialCapacity(100)
    //访问后过期(包括读和写)
    // .expireAfterAccess(30, TimeUnit.SECONDS)
    //写后过期(若expireAfterAccess也存在,则expireAfterWrite为准)
    .expireAfterWrite(30, TimeUnit.SECONDS)
    //写后自动异步刷新
    .refreshAfterWrite(10, TimeUnit.SECONDS)
    //记录下缓存的一些统计数据,例如命中率等
    .recordStats()
    // key弱引用
    // .weakKeys()
    // value弱引用
    // .weakValues()
    // value软引用
    // .softValues()
    //cache对缓存写的通知回调
    .writer(new CacheWriter<Object, Object>() {
      @Override
      public void write(@NonNull Object key, @NonNull Object value) {
        // 持久化或者次级缓存
        log.info("key={}, CacheWriter write", key);
      }

      @Override
      public void delete(@NonNull Object key, @Nullable Object value, @NonNull RemovalCause cause) {
        // 从持久化或者次级缓存中删除
        log.info("key={}, cause={}, CacheWriter delete", key, cause);
      }
    })
    // 移除监听器
    .removalListener((k, v, cause) -> {
      log.info("key={}, value={}, cause={}, remove listener...", k, v, cause);
    })
    //使用CacheLoader创建一个LoadingCache
    .build(this::loadData);
  	// 使用CacheLoader创建一个AsyncLoadingCache
    // .buildAsync(this::loadData);

  TimeUnit.SECONDS.sleep(30);
  Object test = cache.get("test");
  System.out.println(cache.stats());
  System.out.println(test);
  Assert.assertNotNull(test);
}

private Object loadData(String key) {
  Map<String, Object> map = new LinkedHashMap<>();
  List<String> list = new LinkedList<>();
  list.add("111");
  list.add("222");
  list.add("333");
  list.add("444");
  list.add("555");
  list.add("666");
  list.add("777");
  map.put("num", list);
  map.put("test", "test");
  map.put("name", "Bennett");
  map.put("age", 24);
  map.put("sex", "man");
  return map.get(key);
}

CacheWriter注意项

CacheWriter将会在缓存元素被创建,更新或者移除的时候被触发。但是当一个映射被加载(比如LoadingCache.get),重载 (比如LoadingCache.refresh),或者生成 (比如 Map.computeIfPresent) 的时候将不会被触发。

CacheWriter 将不能与 weak keys 或者AsyncLoadingCache在一起使用,AsyncLoadingCache不支持弱引用和软引用。

基于引用的过期方式

Java中四种引用类型

引用类型被垃圾回收时间用途生存时间
强引用 Strong Reference从来不会对象的一般状态JVM停止运行时终止
软引用 Soft Reference在内存不足时对象缓存内存不足时终止
弱引用 Weak Reference在垃圾回收时对象缓存gc运行后终止
虚引用 Phantom Reference从来不会可以用虚引用来跟踪对象被垃圾回收器回收的活动,当一个虚引用关联的对象被垃圾收集器回收之前会收到一条系统通知JVM停止运行时终止

在SpringBoot中应用

添加依赖

  • Maven
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
  <groupId>com.github.ben-manes.caffeine</groupId>
  <artifactId>caffeine</artifactId>
</dependency>
  • Gradle
implementation 'org.springframework.boot:spring-boot-starter-cache'
implementation 'com.github.ben-manes.caffeine:caffeine'

boot配置文件

server:
  port: 9999
spring:
  cache:
    type: caffeine
    cache-names: caffeine-demo,caffeine-demo2
    caffeine:
      spec: initialCapacity=50,maximumSize=256,expireAfterWrite=30s,refreshAfterWrite=10s,recordStats,weakKeys,weakValues

spring.cache.caffeine.spec支持的属性如下

  • initialCapacity=[integer]: 初始的缓存空间大小

  • maximumSize=[long]: 缓存的最大条数

  • maximumWeight=[long]: 缓存的最大权重

  • expireAfterAccess=[duration]: 最后一次写入或访问后经过固定时间过期

  • expireAfterWrite=[duration]: 最后一次写入后经过固定时间过期

  • refreshAfterWrite=[duration]: 创建缓存或者最近一次更新缓存后经过固定的时间间隔,刷新缓存

  • weakKeys: 打开key的弱引用

  • weakValues:打开value的弱引用

  • softValues:打开value的软引用

  • recordStats:开发统计功能

代码配置

package com.bennett.demo.cache.config;

import com.github.benmanes.caffeine.cache.CacheLoader;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * Caffeine配置类
 *
 * @author bennett
 * @date 2020/12/24
 */

@Configuration
public class CaffeineConfig {
  
  /**
  * refreshAfterWrite需要配置一个cacheLoader才可以使用
  */
    @Bean
    public CacheLoader<Object, Object> cacheLoader() {
        return this::loadData;
    }
    private Object loadData(Object key) {
      // 模拟从数据库/次级缓存获取数据
      Map<Object, Object> map = new LinkedHashMap<>();
      List<String> list = new LinkedList<>();
      list.add("111");
      list.add("222");
      list.add("333");
      list.add("444");
      list.add("555");
      list.add("666");
      list.add("777");
      map.put("num", list);
      map.put("test", "test");
      map.put("name", "Bennett");
      map.put("age", 24);
      map.put("sex", "man");
      return map.get(key);
    }
}

应用到代码中

在启动类中使用@EnableCaching注解启用缓存。

@CacheConfig(cacheNames = {"caffeine-demo"})// 指定本类中使用的缓存名
@RestController
public class CaffeineDemoController {

    @GetMapping("/get/test")
    @CachePut(key = "'test'")// key使用的是Spring的SpEL表达式,CachePut是存放添加/更新缓存,无论是否有缓存都会执行方法体
    public String getTest() {
        return "test";
    }


    @Cacheable(key = "'test'", cacheNames = "caffeine-demo")// 也可以在使用时指定缓存名,Cacheable是获取缓存,默认当有对应的缓存存在则不会执行方法体内容,否则会执行方法体,并将方法返回值存入缓存,或者根据条件判断是否执行方法体,以及结果处理
    @GetMapping("/get/cache")
    public String getCache() {
        System.out.println("output cache...");
        return "cache test";
    }

    @GetMapping("/del/cache")
    @CacheEvict(key = "'test'")// 删除缓存
    public String delCache() {
        return "del cache ok!";
    }
    @Cacheable(key = "#user.name + #user.id")
    @PostMapping("/get/user/cache")
    public String getUserCache(@RequestBody User user) {
        System.out.println("output cache...");
        return "cache test";
    }
}

cache方面的注解主要有以下5个:

  • @Cacheable 触发缓存入口(这里一般放在创建和获取的方法上,@Cacheable注解会先查询是否已经有缓存,有会使用缓存,没有则会执行方法并缓存)
  • @CacheEvict 触发缓存的eviction(用于删除的方法上)
  • @CachePut 更新缓存且不影响方法执行(用于修改的方法上,该注解下的方法始终会被执行)
  • @Caching 将多个缓存组合在一个方法上(该注解可以允许一个方法同时设置多个注解)
  • @CacheConfig 在类级别设置一些缓存相关的共同配置(与其它缓存配合使用)

@Cacheable的属性详解,其他两个@CachePut@CacheEvict与之相同。

public @interface Cacheable {

    /**
     * 要使用的cache的名字
     */
    @AliasFor("cacheNames")
    String[] value() default {};

    /**
     * 同value(),决定要使用那个/些缓存
     */
    @AliasFor("value")
    String[] cacheNames() default {};

    /**
     * 使用SpEL表达式来设定缓存的key,如果不设置默认方法上所有参数都会作为key的一部分
     */
    String key() default "";

    /**
     * 用来生成key,与key()不可以共用,是一个bean的名字,这个Bean要返回一个KeyGenerator类型的数据
     */
    String keyGenerator() default "";

    /**
     * 设定要使用的cacheManager,必须先设置好cacheManager的bean,这是使用该bean的名字
     */
    String cacheManager() default "";

    /**
     * 使用cacheResolver来设定使用的缓存,用法同cacheManager,但是与cacheManager不可以同时使用
     */
    String cacheResolver() default "";

    /**
     * 使用SpEL表达式设定出发缓存的条件,在方法执行前生效
     */
    String condition() default "";

    /**
     * 使用SpEL设置出发缓存的条件,这里是方法执行完生效,所以条件中可以有方法执行后的value
     */
    String unless() default "";

    /**
     * 用于同步的,在缓存失效(过期不存在等各种原因)的时候,如果多个线程同时访问被标注的方法
     * 则只允许一个线程通过去执行方法
     */
    boolean sync() default false;
}

使用手动管理缓存

package com.bennett.demo.cache.config;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.CacheLoader;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.CacheManager;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * Caffeine配置类
 *
 * @author bennett
 * @date 2020/12/24
 */

@Configuration
public class CaffeineConfig {

    /**
     * 配置缓存管理器(代码方式配置)
     *
     * @return 缓存管理器
     */
    @Bean("caffeineCacheManager")
    public CacheManager cacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager();
        cacheManager.setCaffeine(Caffeine.newBuilder()
                // 设置最后一次写入或访问后经过固定时间过期
                .expireAfterWrite(60, TimeUnit.SECONDS)
                // 初始的缓存空间大小
                .initialCapacity(100)
                // 缓存的最大条数
                .maximumSize(1000));
        return cacheManager;
    }

    @Bean
    public Cache<String, Object> caffeineCache() {
        return Caffeine.newBuilder()
                // 设置最后一次写入或访问后经过固定时间过期
                .expireAfterWrite(60, TimeUnit.SECONDS)
                // 初始的缓存空间大小
                .initialCapacity(100)
                // 缓存的最大条数
                .maximumSize(1000)
                .build();
    }

    @Bean
    public CacheLoader<Object, Object> cacheLoader() {
        return this::loadData;
    }

    private Object loadData(Object key) {
        Map<Object, Object> map = new LinkedHashMap<>();
        List<String> list = new LinkedList<>();
        list.add("111");
        list.add("222");
        list.add("333");
        list.add("444");
        list.add("555");
        list.add("666");
        list.add("777");
        map.put("num", list);
        map.put("test", "test");
        map.put("name", "Bennett");
        map.put("age", 24);
        map.put("sex", "man");
        return map.get(key);
    }

    @Bean("userKey")
    public KeyGenerator genKey() {
        return (target, method, params)-> {
            return target;
        };
    }
}
@CacheConfig(cacheNames = {"caffeine-demo", "caffeineCacheManager"})
@RestController
public class CaffeineDemoController {

    @Autowired
    private Cache<String, Object> caffeineCache;
  
    @GetMapping("/get/test")
    @CachePut(key = "'test'")// key使用的是Spring的SpEL表达式,CachePut是存放添加/更新缓存,无论是否有缓存都会执行方法体
    public String getTest() {
        return "test";
    }


    @Cacheable(key = "'test'", cacheNames = "caffeine-demo")// 也可以在使用时指定缓存名,Cacheable是获取缓存,默认当有对应的缓存存在则不会执行方法体内容,否则会执行方法体,并将方法返回值存入缓存,或者根据条件判断是否执行方法体,以及结果处理
    @GetMapping("/get/cache")
    public String getCache() {
        System.out.println("output cache...");
        return "cache test";
    }

    @GetMapping("/del/cache")
    @CacheEvict(key = "'test'")// 删除缓存
    public String delCache() {
        return "del cache ok!";
    }
  
    @Cacheable(key = "#user.name + #user.id")
    @PostMapping("/get/user/cache")
    public String getUserCache(@RequestBody User user) {
        System.out.println("output cache...");
        return "cache test";
    }
    /**
    * 手动管理缓存
    */
    @GetMapping("/get")
    public String getCache2() {
        caffeineCache.put("test", "test cache.....");
        // Object test = caffeineCache.getIfPresent("test");
        // caffeineCache.invalidate("test");
        return "test cache.....";
    }
}

附录

SpEL上下文

注意到上面的key使用了SpEL 表达式。Spring Cache提供了一些供我们使用的SpEL上下文数据,下表直接摘自Spring官方文档:

名称位置描述示例
methodNameroot对象当前被调用的方法名#root.methodname
methodroot对象当前被调用的方法#root.method.name
targetroot对象当前被调用的目标对象实例#root.target
targetClassroot对象当前被调用的目标对象的类#root.targetClass
argsroot对象当前被调用的方法的参数列表#root.args[0]
cachesroot对象当前方法调用使用的缓存列表#root.caches[0].name
Argument Name执行上下文当前被调用的方法的参数,如findArtisan(Artisan artisan),可以通过#artsian.id获得参数#artsian.id
result执行上下文方法执行后的返回值(仅当方法执行后的判断有效,如 unless cacheEvict的beforeInvocation=false)#result

SpEL运算符

类型运算符
关系<,>,<=,>=,==,!=,lt,gt,le,ge,eq,ne
算术+,- ,* ,/,%,^
逻辑&&,||,!,and,or,not,between,instanceof
条件?: (ternary),?: (elvis)
正则表达式matches
其他类型?.,?[…],![…],^[…],$[…]

参考文章

  1. Caffeine Cache-高性能Java本地缓存组件
  2. Caffeine高性能设计剖析
  3. Spring Boot缓存实战 Caffeine
  4. SpringBoot 使用 Caffeine 本地缓存