前言

Github:https://github.com/HealerJean

博客:http://blog.healerjean.com

一、简介

JetCache2.0 的核心是 com.alicp.jetcache.Cache

1、注解说明

操作 注解使用
插入 @Cached(name="userCache.", key="#userId", expire = 3600)
更新 ` @CacheUpdate(name=”userCache.”, key=”#user.userId”, value=”#user”)`
删除 @CacheInvalidate(name="userCache.", key="#userId")

@CacheUpdate@CacheInvalidatenamearea 属性必须和 @Cached 相同,name 属性还会用做 cachekey 前缀。

@Cached 注解和 @CreateCache 的属性非常类似,但是多几个:

⬤ 使用 @CacheUpdate@CacheInvalidate 的时候,相关的缓存操作可能会失败(比如网络IO错误),所以指定缓存的超时时间是非常重要的。

1)@Cached

属性 默认值 说明
area “default” 如果在配置中配置了多个缓存 area,在这里指定使用哪个 area
name 未定义 指定缓存的唯一名称,不是必须的,如果没有指定,会使用类名+方法名。name 会被用于远程缓存的 key 前缀。另外在统计中,一个简短有意义的名字会提高可读性。
key 未定义 使用[ SpEL ]指定 key,如果没有指定会根据所有参数自动生成。
expire 未定义 超时时间。如果注解上没有定义,会使用全局配置,如果此时全局配置也没有定义,则为无穷大
timeUnit TimeUnit.SECONDS 指定 expire 的单位
cacheType CacheType.REMOTE 缓存的类型,包括 CacheType.REMOTECacheType.LOCALCacheType.BOTH。如果定义为 BOTH,会使用 LOCALREMOTE 组合成两级缓存
localLimit 未定义 如果 cacheTypeLOCALBOTH,这个参数指定本地缓存的最大元素数量,以控制内存占用。如果注解上没有定义,会使用全局配置,如果此时全局配置也没有定义,则为 100
localExpire 未定义 仅当 cacheTypeBOTH 时适用,为内存中的 Cache指定一个不一样的超时时间,通常应该小于 expire
serialPolicy 未定义 指定远程缓存的序列化方式。可选值为 SerialPolicy.JAVASerialPolicy.KRYO。如果注解上没有定义,会使用全局配置,如果此时全局配置也没有定义,则为 SerialPolicy.JAVA
keyConvertor 未定义 指定 KEY 的转换方式,用于将复杂的 KEY 类型转换为缓存实现可以接受的类型,当前支持 KeyConvertor.FASTJSONKeyConvertor.NONENONE 表示不转换,FASTJSON 可以将复杂对象 KEY 转换成 String。如果注解上没有定义,会使用全局配置。
enabled true 是否激活缓存。例如某个 dao方法上加缓存注解,由于某些调用场景下不能有缓存,所以可以设置 enabledfalse,正常调用不会使用缓存,在需要的地方可使用CacheContext.enableCache 在回调中激活缓存,缓存激活的标记在 ThreadLocal上,该标记被设置后,所有enable=false的缓存都被激活
cacheNullValue false 当方法返回值为 null 的时候是否要缓存
condition 未定义 使用[SpEL]指定条件,如果表达式返回 true 的时候才去缓存中查询
postCondition 未定义 使用[SpEL]指定条件,如果表达式返回 true 的时候才更新缓存,该评估在方法执行后进行,因此可以访问到 #result

2)@CacheInvalidate

属性 默认值 说明
area “default” 如果在配置中配置了多个缓存 area,在这里指定使用哪个 area,指向对应的 @Cached定义。
name 未定义 指定缓存的唯一名称,指向对应的 @Cached 定义。
key 未定义 使用 [SpEL] 指定 key
condition 未定义 使用 [SpEL] 指定条件,如果表达式返回 true 才执行删除,可访问方法结果#result

3)@CacheUpdate

属性 默认值 说明
area “default” 如果在配置中配置了多个缓存 area,在这里指定使用哪个area,指向对应的 @Cached 定义。
name 未定义 指定缓存的唯一名称,指向对应的 @Cached 定义。
key 未定义 使用[SpEL]指定 key
value 未定义 使用[SpEL]指定 value
condition 未定义 使用[SpEL]指定条件,如果表达式返回true才执行更新,可访问方法结果#result

4)@CacheRefresh

属性 默认值 说明
refresh 未定义 刷新间隔
timeUnit TimeUnit.SECONDS 时间单位
stopRefreshAfterLastAccess 未定义 指定该 key 多长时间没有访问就停止刷新,如果不指定会一直刷新
refreshLockTimeout 60 类型为 BOTH / REMOTE 的缓存刷新时,同时只会有一台服务器在刷新,这台服务器会在远程缓存放置一个分布式锁,此配置指定该锁的超时时间

5)@CachePenetrationProtect

当缓存访问未命中的情况下,对并发进行的加载行为进行保护。 当前版本实现的是单 JVM内的保护,即同一个 JVM 中同一个 key只有一个线程去加载,其它线程等待结果。

2、配置详解

jetcache:
  statIntervalMinutes: 15
  areaInCacheName: false
  hidePackages: com.alibaba
  local:
    default:
      type: caffeine
      limit: 100
      keyConvertor: fastjson2 #其他可选:fastjson/jackson
      expireAfterWriteInMillis: 100000
    otherArea:
      type: linkedhashmap
      limit: 100
      keyConvertor: none
      expireAfterWriteInMillis: 100000
  remote:
    default:
      type: redis
      keyConvertor: fastjson2 #其他可选:fastjson/jackson
      broadcastChannel: projectA
      valueEncoder: java #其他可选:kryo/kryo5
      valueDecoder: java #其他可选:kryo/kryo5
      poolConfig:
        minIdle: 5
        maxIdle: 20
        maxTotal: 50
      host: ${redis.host}
      port: ${redis.port}
    otherArea:
      type: redis
      keyConvertor: fastjson2 #其他可选:fastjson/jackson
      broadcastChannel: projectA
      valueEncoder: java #其他可选:kryo/kryo5
      valueDecoder: java #其他可选:kryo/kryo5
      poolConfig:
        minIdle: 5
        maxIdle: 20
        maxTotal: 50
      host: ${redis.host}
      port: ${redis.port}
属性 默认值 说明
jetcache.statIntervalMinutes 0 统计间隔,0 表示不统计
jetcache.areaInCacheName true(2.6-) false(2.7+) jetcache-annocacheName 作为远程缓存 key前缀,2.4.3以前的版本总是把 areaName 加在 cacheName 中,因此areaName 也出现在 key 前缀中。2.4.4以后可以配置,为了保持远程 key兼容默认值为 true,但是新项目的话 false更合理些,2.7 默认值已改为 false
jetcache.hiddenPackages @Cached@CreateCache自动生成 name的时候,为了不让name 太长,hiddenPackages指定的包名前缀被截掉
jetcache.[local/remote].${area}.type 缓存类型。tairredis 为当前支持的远程缓存;linkedhashmapcaffeine 为当前支持的本地缓存类型
jetcache.[local/remote].${area}.keyConvertor fastjson2 key 转换器的全局配置,2.6.5+已经支持的 keyConvertorfastjson2/jackson; 2.6.5-只有一个已经实现的 keyConvertorfastjson。仅当使用 @CreateCache且缓存类型为 LOCAL 时可以指定为 none,此时通过 equals方法来识别 key。方法缓存必须指定 keyConvertor
jetcache.[local/remote].${area}.valueEncoder java 序列化器的全局配置。仅 remote 类型的缓存需要指定,2.7+可选java/kryo/kryo5;2.6-可选java/kryo
jetcache.[local/remote].${area}.valueDecoder java 序列化器的全局配置。仅 remote类型的缓存需要指定,2.7+可选java/kryo/kryo5;2.6-可选java/kryo
jetcache.[local/remote].${area}.limit 100 每个缓存实例的最大元素的全局配置,仅 local 类型的缓存需要指定。注意是每个缓存实例的限制,而不是全部,比如这里指定 100,然后用@CreateCache 创建了两个缓存实例(并且注解上没有设置 localLimit 属性),那么每个缓存实例的限制都是100
jetcache.[local/remote].${area}.expireAfterWriteInMillis 无穷大 以毫秒为单位指定超时时间的全局配置(以前为defaultExpireInMillis)
jetcache.remote.${area}.broadcastChannel jetcahe2.7 的两级缓存支持更新以后失效其他 JVM 中的 local cache,但多个服务共用 redis同一个 channel 可能会造成广播风暴,需要在这里指定channel,你可以决定多个不同的服务是否共用同一个 channel。如果没有指定则不开启。
jetcache.local.${area}.expireAfterAccessInMillis 0 需要 jetcache2.2 以上,以毫秒为单位,指定多长时间没有访问,就让缓存失效,当前只有本地缓存支持。0 表示不使用这个功能。

3、API 缓存创建

JetCache2 版本的 @Cached 注解是基于 Spring4.X 版本实现的,在没有 Spring 支持的情况下,注解将不能使用。但是可以直接使用 JetCacheAPI 来创建、管理、监控 Cache,多级缓存也可以使用。

1)快速创建

@Bean
public Cache<Long, Object> getUserCache(CacheManager cacheManager) {
  QuickConfig qc = QuickConfig.newBuilder("userCache:").expire(Duration.ofSeconds(3600))
          // 创建一个两级缓存
          .cacheType(CacheType.REMOTE)
          // 本地缓存元素个数限制,只对CacheType.LOCAL和CacheType.BOTH有效
          //.localLimit(100)
          // 本地缓存更新后,将在所有的节点中删去缓存,以保持强一致性
          // .syncLocal(false)
          .build();
  return cacheManager.getOrCreateCache(qc);
}

2)LinkedHashMapCache

Cache<String, Integer> cache = LinkedHashMapCacheBuilder.createLinkedHashMapCacheBuilder()
                .limit(100)
                .expireAfterWrite(200, TimeUnit.SECONDS)
                .buildCache();

3)CaffeineCache

Cache<Long, OrderDO> cache = CaffeineCacheBuilder.createCaffeineCacheBuilder()
                .limit(100)
                .expireAfterWrite(200, TimeUnit.SECONDS)
                .buildCache();

4)RedisCache

GenericObjectPoolConfig pc = new GenericObjectPoolConfig();
        pc.setMinIdle(2);
        pc.setMaxIdle(10);
        pc.setMaxTotal(10);
        JedisPool pool = new JedisPool(pc, "localhost", 6379);
Cache<Long, OrderDO> orderCache = RedisCacheBuilder.createRedisCacheBuilder()
                .keyConvertor(Fastjson2KeyConvertor.INSTANCE)
                .valueEncoder(JavaValueEncoder.INSTANCE)
                .valueDecoder(JavaValueDecoder.INSTANCE)
                .jedisPool(pool)
                .keyPrefix("orderCache")
                .expireAfterWrite(200, TimeUnit.SECONDS)
                .buildCache();

5)多级缓存

Cache multiLevelCache = MultiLevelCacheBuilder.createMultiLevelCacheBuilder()
      .addCache(memoryCache, redisCache)
      .expireAfterWrite(100, TimeUnit.SECONDS)
      .buildCache();

4、高阶API

1)异步API

JetCache2.2 版本开始,所有的大写 API 返回的 CacheResult 都支持异步。当底层的缓存实现支持异步的时候,大写 API 返回的结果都是异步的。当前支持异步的实现只有 jetcacheredis-luttece实现,其他的缓存实现(内存中的、Tair、Jedis等),所有的异步接口都会同步堵塞,这样 API 仍然是兼容的。

CacheGetResult<UserDO> r = cache.GET(userId);

这一行代码执行完以后,缓存操作可能还没有完成,如果此时调用 r.isSuccess() 或者 r.getValue()或者 r.getMessage()将会堵塞直到缓存操作完成。如果不想被堵塞,并且需要在缓存操作完成以后执行后续操作,可以这样做:

CompletionStage<ResultData> future = r.future();
future.thenRun(() -> {
    if(r.isSuccess()){
        System.out.println(r.getValue());
    }
});

以上代码将会在缓存操作异步完成后,在完成异步操作的线程中调用 thenRun中指定的回调。CompletionStageJava8 新增的功能,如果对此不太熟悉可以先查阅相关的文档。需要注意的是,既然已经选择了异步的开发方式,在回调中不能调用堵塞方法,以免堵塞其他的线程(回调方法很可能是在 event loop 线程中执行的)。

部分小写的 api 不需要任何修改,就可以直接享受到异步开发的好处。比如 putremoveAll 方法,由于它们没有返回值,所以此时就直接优化成异步调用,能够减少 RT;而 get 方法由于需要取返回值,所以仍然会堵塞。

2)自动 load

LoadingCache 类提供了自动 load 的功能,它是一个包装,基于 decorator 模式,也实现了 Cache 接口。如果 CacheBuilder指定了 loader,那么 buildCache 返回的 Cache 实例就是经过 LoadingCache 包装过的。例如:

Cache<Long,UserDO> userCache = LinkedHashMapCacheBuilder.createLinkedHashMapCacheBuilder()
                .loader(key -> loadUserFromDatabase(key))
                .buildCache();

LoadingCachegetgetAll 方法,在缓存未命中的情况下,会调用 loader,如果 loader 抛出异常,getgetAll 会抛出CacheInvokeException

需要注意

1、GETGET_ALL 这类大写 API 只纯粹访问缓存,不会调用 loader

2、如果使用多级缓存,loader 应该安装在 MultiLevelCache 上,不要安装在底下的缓存上

3)自动刷新缓存

JetCache2.2 版本开始,RefreshCache 基于decorator 模式提供了自动刷新的缓存的能力,目的是为了防止缓存失效时造成的雪崩效应打爆数据库。同时设置了 loaderrefreshPolicy 的时候,CacheBuilderbuildCache 方法返回的 Cache实例经过了RefreshCache 的包装。

对一些 key 比较少,实时性要求不高,加载开销非常大的缓存场景,适合使用自动刷新。上面的代码指定每分钟刷新一次,30 分钟如果没有访问就停止刷新。如果缓存是 redis 或者多级缓存最后一级是 redis,缓存加载行为是全局唯一的,也就是说不管有多少台服务器,同时只有一个服务器在刷新,这是通过 tryLock 实现的,目的是为了降低后端的加载负担。

RefreshPolicy policy = RefreshPolicy.newPolicy(1, TimeUnit.MINUTES)
                .stopRefreshAfterLastAccess(30, TimeUnit.MINUTES);
Cache<String, Long> orderSumCache = LinkedHashMapCacheBuilder
                .createLinkedHashMapCacheBuilder()
                .loader(key -> loadOrderSumFromDatabase(key))
                .refreshPolicy(policy)
                .buildCache();

二、创建Cache

1、CacheManager

使用 CacheManager 可以创建 Cache实例,areaname相同的情况下,它和 Cached 注解使用同一个Cache实例。

1)application-jetcache-config.yml

jetcache:
  # 计算距离,0表明不计算,敞开后定时在控制台输出缓存信息
  statIntervalMinutes: 15
  # 是否把cacheName作为长途缓存key前缀
  areaInCacheName: false
  # 本地缓存装备
  local:
    # default表明全部收效,也能够指定某个cacheName
    default:
      # 本地缓存类型,其他可选:caffeine/linkedhashmap
      type: linkedhashmap
      keyConvertor: fastjson
  # 长途缓存装备
  remote:
    default: # default表明全部收效,也能够指定某个cacheName
      type: redis
      # key转换器办法n
      keyConvertor: fastjson
      broadcastChannel: projectA
      # redis序列化办法
      valueEncoder: java
      valueDecoder: java
      # redis线程池
      poolConfig:
        minIdle: 5
        maxIdle: 20
        maxTotal: 50
      # redis地址与端口
      host: 127.0.0.1
      port: 6379

2)JetCacheConfig

@Configuration
public class JetCacheConfig {

    @Bean
    public Cache<Long, Object> getUserCache(CacheManager cacheManager) {
        QuickConfig qc = QuickConfig.newBuilder("userCache:").expire(Duration.ofSeconds(3600))
                .cacheNullValue(Boolean.TRUE)
                // 创建一个两级缓存
                .cacheType(CacheType.REMOTE)
                // 本地缓存元素个数限制,只对CacheType.LOCAL和CacheType.BOTH有效
                //.localLimit(100)
                // 本地缓存更新后,将在所有的节点中删去缓存,以保持强一致性
                // .syncLocal(false)
                .build();
        return cacheManager.getOrCreateCache(qc);
    }
}

3)缓存操作-硬编码

操作 方法  
插入 || 更新缓存 userCache.put(userDemoBo.getId(), userDemoVO);  
删除缓存 userCache.remove(id);  
获取缓存 userCache.get(userId)  
package com.healerjean.proj.controller;

import com.alicp.jetcache.Cache;
import com.alicp.jetcache.anno.CacheInvalidate;
import com.healerjean.proj.common.anno.ElParam;
import com.healerjean.proj.common.anno.LogIndex;
import com.healerjean.proj.common.data.ValidateGroup;
import com.healerjean.proj.common.data.bo.BaseRes;
import com.healerjean.proj.data.bo.UserDemoBO;
import com.healerjean.proj.data.convert.UserConverter;
import com.healerjean.proj.data.req.UserDemoSaveReq;
import com.healerjean.proj.data.vo.UserDemoVO;
import com.healerjean.proj.exceptions.ParameterException;
import com.healerjean.proj.service.UserDemoService;
import com.healerjean.proj.utils.validate.ValidateUtils;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;

/**
 * JetCacheController
 *
 * @author zhangyujin
 * @date 2023/11/21
 */
@RestController
@RequestMapping("api/jetcache")
@Api(tags = "JetCacheController-控制器")
@Slf4j
public class JetCacheController {

    /**
     * userDemoService
     */
    @Resource
    private UserDemoService userDemoService;
    /**
     * userCache
     */
    @Resource
    private Cache<Long, Object> userCache;


    @ApiOperation("用户信息-新增")
    @LogIndex
    @PostMapping("user/save")
    @ResponseBody
    public BaseRes<UserDemoVO> saveUserDemo(@ElParam("#req.name") @RequestBody UserDemoSaveReq req) {
        String errorMessage = ValidateUtils.validate(req, ValidateGroup.SaveUserDemo.class);
        if (!ValidateUtils.COMMON_SUCCESS.equals(errorMessage)) {
            throw new ParameterException(errorMessage);
        }
        UserDemoBO userDemoBo = UserConverter.INSTANCE.covertUserDemoSaveReqToBo(req);
        boolean success = userDemoService.saveUserDemo(userDemoBo);
        if (Boolean.FALSE.equals(success)) {
            return BaseRes.buildFailure();
        }
        UserDemoVO userDemoVO = UserConverter.INSTANCE.covertUserDemoBoToVo(userDemoBo);
        // 缓存放入
        userCache.put(userDemoBo.getId(), userDemoVO);
        return BaseRes.buildSuccess(userDemoVO);
    }


    @CacheInvalidate(name = "userCache:", key = "#id")
    @ApiOperation("用户信息-删除")
    @LogIndex
    @DeleteMapping("user/{id}")
    public BaseRes<Boolean> deleteUserDemo(@PathVariable Long id) {
        boolean success = userDemoService.deleteUserDemo(id);
        if (Boolean.FALSE.equals(success)) {
            return BaseRes.buildSuccess(Boolean.FALSE);
        }
        // 缓存删除
        userCache.remove(id);
        return BaseRes.buildSuccess(success);
    }

    @ApiOperation("用户信息-修改")
    @LogIndex
    @PutMapping("user/{id}")
    @ResponseBody
    public BaseRes<UserDemoVO> updateUserDemo(@PathVariable Long id, @RequestBody UserDemoSaveReq req) {
        UserDemoBO userDemoBo = UserConverter.INSTANCE.covertUserDemoSaveReqToBo(req);
        userDemoBo.setId(id);
        boolean success = userDemoService.updateUserDemo(userDemoBo);
        if (Boolean.FALSE.equals(success)) {
            return BaseRes.buildFailure();
        }
        UserDemoVO userDemoVO = UserConverter.INSTANCE.covertUserDemoBoToVo(userDemoBo);
        // 缓存更新
        userCache.put(userDemoBo.getId(), userDemoVO);
        return BaseRes.buildSuccess(userDemoVO);
    }

    @LogIndex
    @ApiOperation("用户信息-单条查询")
    @GetMapping("user/{userId}")
    @ResponseBody
    public BaseRes<UserDemoVO> queryUserDemoSingle(@ElParam @PathVariable("userId") Long userId) {
        // 允许缓存空值
        CacheGetResult<Object> cacheGetResult = userCache.GET(userId);
        if (cacheGetResult.isSuccess()){
            return BaseRes.buildSuccess((UserDemoVO) cacheGetResult.getValue());
        }

        UserDemoBO userDemoBo = userDemoService.selectById(userId);
        UserDemoVO userDemoVo = UserConverter.INSTANCE.covertUserDemoBoToVo(userDemoBo);
        // 缓存插入
        userCache.put(userId, userDemoVo);
        return BaseRes.buildSuccess(userDemoVo);
    }


4)缓存操作-注解

操作 注解使用
插入 @Cached(name="userCache.", key="#userId", expire = 3600)
更新 ` @CacheUpdate(name=”userCache.”, key=”#user.userId”, value=”#user”)`
删除 @CacheInvalidate(name="userCache.", key="#userId")

@CacheUpdate@CacheInvalidatenamearea 属性必须和 @Cached 相同,name 属性还会用做 cachekey 前缀。

@Cached 注解和 @CreateCache 的属性非常类似,但是多几个:

/**
 * selectById
 *
 * @param id id
 * @return UserDemoBO
 */
@Cached(name="userCache:", key="#id", expire = 3600)
@Override
public UserDemoBO selectById(Long id) {
    UserDemo userDemo = userDemoManager.selectById(id);
    return UserConverter.INSTANCE.covertUserDemoPoToBo(userDemo);
}


/**
 * deleteUserDemo
 *
 * @param id id
 * @return boolean
 */
@CacheInvalidate(name="userCache:", key="#id")
@Override
public boolean deleteUserDemo(Long id) {
    UserDemo userDemo = new UserDemo();
    userDemo.setId(id);
    userDemo.setValidFlag(SystemEnum.StatusEnum.TRASH.getCode());
    return userDemoManager.deleteUserDemo(userDemo);
}

5、统计

yml 中的 jetcache.statIntervalMinutes 大于 0 时,通过 @CreateCache@Cached 配置出来的 Cache 自带监控。JetCache 会按指定的时间定期通过 logger 输出统计信息。默认输出信息类似如下:

2017-01-12 19:00:00,001 INFO  support.StatInfoLogger - jetcache stat from 2017-01-12 18:59:00,000 to 2017-01-12 19:00:00,000
cache                                                |       qps|   rate|           get|           hit|          fail|        expire|avgLoadTime|maxLoadTime
-----------------------------------------------------+----------+-------+--------------+--------------+--------------+--------------+-----------+-----------
default_AlicpAppChannelManager.getAlicpAppChannelById|      0.00|  0.00%|             0|             0|             0|             0|        0.0|          0
default_ChannelManager.getChannelByAccessToten       |     30.02| 99.78%|         1,801|         1,797|             0|             4|        0.0|          0
default_ChannelManager.getChannelByAppChannelId      |      8.30| 99.60%|           498|           496|             0|             1|        0.0|          0

ContactAuthor