一、JetCache
JetCache是一个基于Java的缓存系统封装,提供统一的API和注解来简化缓存的使用。 JetCache提供了比SpringCache更加强大的注解,可以原生的支持TTL、两级缓存、分布式自动刷新,还提供了Cache接口用于手工缓存操作。 当前有四个实现,RedisCache、TairCache(此部分未在github开源)、CaffeineCache(in memory)和一个简易的LinkedHashMapCache(in memory),要添加新的实现也是非常简单的。
全部特性:
- 通过统一的API访问Cache系统
- 通过注解实现声明式的方法缓存,支持TTL和两级缓存
- 通过注解创建并配置Cache实例
- 针对所有Cache实例和方法缓存的自动统计
- Key的生成策略和Value的序列化策略是可以配置的
- 分布式缓存自动刷新,分布式锁 (2.2+)
- 异步Cache API (2.2+,使用Redis的lettuce客户端时)
- Spring Boot支持
github地址: https://github.com/alibaba/jetcache
注意:
JetCache需要JDK1.8、Spring Framework4.0.8以上版本。Spring Boot为可选,需要1.1.9以上版本。如果不使用注解(仅使用jetcache-core),Spring Framework也是可选的,此时使用方式与Guava/Caffeine cache类似。
二、SpringBoot 引入
- pom
<!-- https://mvnrepository.com/artifact/com.alicp.jetcache/jetcache-starter-redis-lettuce -->
<dependency>
<groupId>com.alicp.jetcache</groupId>
<artifactId>jetcache-starter-redis-lettuce</artifactId>
<version>2.6.0.M1</version>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<!-- 添加对redis的支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 添加对连接池的支持 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
- application.yml
jetcache:
statIntervalMinutes: 1 #统计间隔
areaInCacheName: false
local:
default: #默认area
type: caffeine
keyConvertor: fastjson
remote:
default:
type: redis.lettuce #使用lettuce
keyConvertor: fastjson
valueEncoder: java
valueDecoder: java
poolConfig:
minIdle: 1
maxIdle: 50
maxTotal: 1000
maxWait: 1000
# uri: ['redis://password@192.168.0.1:6379/0','redis://password@192.168.0.2:6379/0','redis://password@192.168.0.3:6379/0']
uri:
- redis://password@192.168.0.1:6379/0 #redis://密码@IP:端口/库
- redis://password@192.168.0.2:6379/0
- redis://password@192.168.0.3:6379/0
readFrom: masterPreferred #master优先
- 启动类开启缓存
@SpringBootApplication
@EnableCreateCacheAnnotation
@EnableMethodCache(basePackages = { "com.bxc.jetcache.jetcachedemo.service" })
public class JetcacheDemoApplication {
public static void main(String[] args) {
SpringApplication.run(JetcacheDemoApplication.class, args);
}
}
三、方法缓存
1. @Cached
使用@Cached方法可以为一个方法添加上缓存。JetCache通过Spring AOP生成代理,来支持缓存功能。注解可以加在接口方法上也可以加在类方法上,但需要保证是个Spring bean。
@Cached(name = "bxcCache:", cacheType = CacheType.BOTH, key = "#id", expire = 30, timeUnit = TimeUnit.MINUTES)
public String getCache(String id) {
System.out.println("哈哈哈哈");
return "abc";
}
@CacheUpdate(name = "bxcCache:", key = "#id", value = "#result")
public String updateCache(String id) {
return "update";
}
@CacheInvalidate(name = "bxcCache:", key = "#id")
public String deleteCache(String id){
return "delete";
}
上面代码中可以看出,我们可以使用SpEL(Spring Expression Language)来设置key和Value。name属性不是必须的,但是起个名字是个好习惯,展示统计数据的使用,会使用这个名字。如果同一个area两个@CreateCache的name配置一样,它们生成的Cache将指向同一个实例。这里面需要注意的是,java代码的编辑级别必须是1.8。
其中CacheType
CacheType.REMOTE 远程缓存,如:redis
CacheType.LOCAL本地缓存,如:linkedhashmap、caffeine
CacheType.BOTH 本地+远程缓存,二级缓存
@CacheUpdate和@CacheInvalidate的name和area属性必须和@Cached相同,name属性还会用做cache的key前缀。
@Cached注解和@CreateCache的属性非常类似,但是多几个:
属性 | 默认值 | 说明 |
---|---|---|
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. @CacheRefresh 自动刷新
@CacheRefresh 相当于开始一个定时任务,定时刷新值到缓存中。
@Cached(name = "bxcCache:", cacheType = CacheType.BOTH, key = "#id", expire = 30, timeUnit = TimeUnit.MINUTES)
@CacheRefresh(refresh = 2, stopRefreshAfterLastAccess = 10, timeUnit = TimeUnit.SECONDS)
public String getCache(String id) {
System.out.println("哈哈哈哈");
return "abc";
}
refresh 刷新时间周期
stopRefreshAfterLastAccess 停止刷新周期
timeUnit 时间单位
运行效果:
@CacheRefresh注解说明:
属性 | 默认值 | 说明 |
---|---|---|
refresh | 未定义 | 刷新间隔 |
timeUnit | TimeUnit.SECONDS | 时间单位 |
stopRefreshAfterLastAccess | 未定义 | 指定该key多长时间没有访问就停止刷新,如果不指定会一直刷新 |
refreshLockTimeout | 60秒 | 类型为BOTH/REMOTE的缓存刷新时,同时只会有一台服务器在刷新,这台服务器会在远程缓存放置一个分布式锁,此配置指定该锁的超时时间 |
3. @CachePenetrationProtect 同步加载数据
当缓存访问未命中的情况下,对并发进行的加载行为进行保护。 当前版本实现的是单JVM内的保护,即同一个JVM中同一个key只有一个线程去加载,其它线程等待结果。
@Cached(name = "bxcCache:", cacheType = CacheType.BOTH, key = "#id", expire = 30, timeUnit = TimeUnit.MINUTES)
@CachePenetrationProtect
public String getCache(String id) {
System.out.println("哈哈哈哈");
return "abc";
}
参数解析:
四、缓存API
使用@CreateCache注解去创建一个Cache实例
@CreateCache(expire = 100, cacheType = CacheType.BOTH, localLimit = 50)
private Cache<Long, UserDO> userCache;
expire 超时时间
cacheType 缓存类型
localLimit 限制缓存数量
//写入缓存
cache.put("abc", "aaa");
//读取缓存
System.out.println("1 > " + cache.get("abc"));
System.out.println("2 > "+ cache.computeIfAbsent("abc1", res -> {
return "111";
}));
System.out.println("3 > " + cache.computeIfAbsent("abc2", res -> {
return "222";
}, false, 10, TimeUnit.SECONDS));
五、异步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方法由于需要取返回值,所以仍然会堵塞。
六、分布式锁
boolean hasRun = cache.tryLockAndRun("liuxw3", 100, TimeUnit.SECONDS, () -> {
System.out.println("我获取到锁了");
});