本地缓存_之_jetcache
前言
Github:https://github.com/HealerJean
一、简介
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
和 @CacheInvalidate
的 name
和 area
属性必须和 @Cached
相同,name
属性还会用做 cache
的 key
前缀。
⬤ @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.REMOTE 、CacheType.LOCAL 、CacheType.BOTH 。如果定义为 BOTH ,会使用 LOCAL 和 REMOTE 组合成两级缓存 |
localLimit |
未定义 | 如果 cacheType 为 LOCAL 或 BOTH ,这个参数指定本地缓存的最大元素数量,以控制内存占用。如果注解上没有定义,会使用全局配置,如果此时全局配置也没有定义,则为 100 |
localExpire |
未定义 | 仅当 cacheType 为 BOTH 时适用,为内存中的 Cache 指定一个不一样的超时时间,通常应该小于 expire |
serialPolicy |
未定义 | 指定远程缓存的序列化方式。可选值为 SerialPolicy.JAVA 和 SerialPolicy.KRYO 。如果注解上没有定义,会使用全局配置,如果此时全局配置也没有定义,则为 SerialPolicy.JAVA |
keyConvertor |
未定义 | 指定 KEY 的转换方式,用于将复杂的 KEY 类型转换为缓存实现可以接受的类型,当前支持 KeyConvertor.FASTJSON 和 KeyConvertor.NONE 。NONE 表示不转换,FASTJSON 可以将复杂对象 KEY 转换成 String 。如果注解上没有定义,会使用全局配置。 |
enabled |
true |
是否激活缓存。例如某个 dao 方法上加缓存注解,由于某些调用场景下不能有缓存,所以可以设置 enabled 为 false ,正常调用不会使用缓存,在需要的地方可使用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-anno 把 cacheName 作为远程缓存 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 |
无 | 缓存类型。tair 、redis 为当前支持的远程缓存;linkedhashmap 、caffeine 为当前支持的本地缓存类型 |
jetcache.[local/remote].${area}.keyConvertor |
fastjson2 |
key 转换器的全局配置,2.6.5+已经支持的 keyConvertor :fastjson2 /jackson ; 2.6.5-只有一个已经实现的 keyConvertor :fastjson 。仅当使用 @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
支持的情况下,注解将不能使用。但是可以直接使用JetCache
的API
来创建、管理、监控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
返回的结果都是异步的。当前支持异步的实现只有jetcache
的redis-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
中指定的回调。CompletionStage
是Java8
新增的功能,如果对此不太熟悉可以先查阅相关的文档。需要注意的是,既然已经选择了异步的开发方式,在回调中不能调用堵塞方法,以免堵塞其他的线程(回调方法很可能是在event
loop
线程中执行的)。部分小写的
api
不需要任何修改,就可以直接享受到异步开发的好处。比如put
和removeAll
方法,由于它们没有返回值,所以此时就直接优化成异步调用,能够减少RT
;而get
方法由于需要取返回值,所以仍然会堵塞。
2)自动 load
LoadingCache
类提供了自动load
的功能,它是一个包装,基于decorator
模式,也实现了Cache
接口。如果CacheBuilder
指定了loader
,那么buildCache
返回的Cache
实例就是经过LoadingCache
包装过的。例如:
Cache<Long,UserDO> userCache = LinkedHashMapCacheBuilder.createLinkedHashMapCacheBuilder()
.loader(key -> loadUserFromDatabase(key))
.buildCache();
LoadingCache
的get
和getAll
方法,在缓存未命中的情况下,会调用loader
,如果loader
抛出异常,get
和getAll
会抛出CacheInvokeException
。
需要注意
1、GET
、GET
_ALL
这类大写 API
只纯粹访问缓存,不会调用 loader
。
2、如果使用多级缓存,loader
应该安装在 MultiLevelCache
上,不要安装在底下的缓存上。
3)自动刷新缓存
从
JetCache2.2
版本开始,RefreshCache
基于decorator
模式提供了自动刷新的缓存的能力,目的是为了防止缓存失效时造成的雪崩效应打爆数据库。同时设置了loader
和refreshPolicy
的时候,CacheBuilder
的buildCache
方法返回的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
实例,area
和name
相同的情况下,它和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
和 @CacheInvalidate
的 name
和 area
属性必须和 @Cached
相同,name
属性还会用做 cache
的 key
前缀。
@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