JetCache是一个基于Java的缓存系统封装,提供统一的API和注解来简化缓存的使用。 JetCache提供了比SpringCache更加强大的注解,可以原生的支持TTL、两级缓存、分布式自动刷新,还提供了Cache
接口用于手工缓存操作。当前有四个实现,RedisCache
、TairCache
(此部分未在github开源)、CaffeineCache
(in memory)和一个简易的LinkedHashMapCache
(in memory),要添加新的实现也是非常简单的。
全部特性:
Cache
实例Cache
实例和方法缓存的自动统计JetCache需要JDK1.8、Spring Framework4.0.8以上版本。Spring Boot为可选,需要1.1.9以上版本。如果不使用注解(仅使用jetcache-core),Spring Framework也是可选的,此时使用方式与Guava/Caffeinecache类似。
Cache
实例LinkedHashMapCache
和CaffeineCache
Cache
Cache
,不依赖Spring。两个内存中的缓存实现LinkedHashMapCache
和CaffeineCache
也由它提供。通过@CreateCache注解创建一个缓存实例,默认超时时间是100秒
@CreateCache(expire = 100)
private Cache userCache;
用起来就像map一样
UserDO user = userCache.get(123L);
userCache.put(123L, user);
userCache.remove(123L);
创建一个两级(内存+远程)的缓存,内存中的元素个数限制在50个。
@CreateCache(name = "UserService.userCache", expire = 100, cacheType = CacheType.BOTH, localLimit = 50)
private Cache userCache;
name属性不是必须的,但是起个名字是个好习惯,展示统计数据的使用,会使用这个名字。如果同一个area两个@CreateCache的name配置一样,它们生成的Cache将指向同一个实例。
使用@Cached方法可以为一个方法添加上缓存。JetCache通过Spring AOP生成代理,来支持缓存功能。注解可以加在接口方法上也可以加在类方法上,但需要保证是个Springbean。
public interface UserService {
@Cached(name="UserService.getUserById", expire = 3600)
User getUserById(long userId);
}
如果使用SpringBoot,可以按如下的方式配置。
com.alicp.jetcache
jetcache-starter-redis
2.4.4
配置一个springboot风格的application.yml文件,把他放到资源目录中
jetcache:
statIntervalMinutes: 15
areaInCacheName: false
local:
default:
type: linkedhashmap
keyConvertor: fastjson
remote:
default:
type: redis
keyConvertor: fastjson
valueEncoder: java
valueDecoder: java
poolConfig:
minIdle: 5
maxIdle: 20
maxTotal: 50
host: 127.0.0.1
port: 6379
然后创建一个App类放在业务包的根下,EnableMethodCache,EnableCreateCacheAnnotation这两个注解分别激活Cached和CreateCache注解,其他和标准的SpringBoot程序是一样的。这个类可以直接main方法运行。
package com.company.mypackage;
import com.alicp.jetcache.anno.config.EnableCreateCacheAnnotation;
import com.alicp.jetcache.anno.config.EnableMethodCache;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@EnableMethodCache(basePackages = "com.company.mypackage")
@EnableCreateCacheAnnotation
public class MySpringBootApp {
public static void main(String[] args) {
SpringApplication.run(MySpringBootApp.class);
}
}
如果没有使用springboot,可以按下面的方式配置(这里使用jedis客户端连接redis为例)。
com.alicp.jetcache
jetcache-anno
2.4.4
com.alicp.jetcache
jetcache-redis
2.4.4
配置了这个JetCacheConfig类以后,可以使用@CreateCache和@Cached注解。
package com.company.mypackage;
import java.util.HashMap;
import java.util.Map;
import com.alicp.jetcache.anno.CacheConsts;
import com.alicp.jetcache.anno.config.EnableCreateCacheAnnotation;
import com.alicp.jetcache.anno.config.EnableMethodCache;
import com.alicp.jetcache.anno.support.GlobalCacheConfig;
import com.alicp.jetcache.anno.support.SpringConfigProvider;
import com.alicp.jetcache.embedded.EmbeddedCacheBuilder;
import com.alicp.jetcache.embedded.LinkedHashMapCacheBuilder;
import com.alicp.jetcache.redis.RedisCacheBuilder;
import com.alicp.jetcache.support.FastjsonKeyConvertor;
import com.alicp.jetcache.support.JavaValueDecoder;
import com.alicp.jetcache.support.JavaValueEncoder;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.util.Pool;
@Configuration
@EnableMethodCache(basePackages = "com.company.mypackage")
@EnableCreateCacheAnnotation
public class JetCacheConfig {
@Bean
public Pool pool(){
GenericObjectPoolConfig pc = new GenericObjectPoolConfig();
pc.setMinIdle(2);
pc.setMaxIdle(10);
pc.setMaxTotal(10);
return new JedisPool(pc, "localhost", 6379);
}
@Bean
public SpringConfigProvider springConfigProvider() {
return new SpringConfigProvider();
}
@Bean
public GlobalCacheConfig config(SpringConfigProvider configProvider, Pool pool){
Map localBuilders = new HashMap();
EmbeddedCacheBuilder localBuilder = LinkedHashMapCacheBuilder
.createLinkedHashMapCacheBuilder()
.keyConvertor(FastjsonKeyConvertor.INSTANCE);
localBuilders.put(CacheConsts.DEFAULT_AREA, localBuilder);
Map remoteBuilders = new HashMap();
RedisCacheBuilder remoteCacheBuilder = RedisCacheBuilder.createRedisCacheBuilder()
.keyConvertor(FastjsonKeyConvertor.INSTANCE)
.valueEncoder(JavaValueEncoder.INSTANCE)
.valueDecoder(JavaValueDecoder.INSTANCE)
.jedisPool(pool);
remoteBuilders.put(CacheConsts.DEFAULT_AREA, remoteCacheBuilder);
GlobalCacheConfig globalCacheConfig = new GlobalCacheConfig();
globalCacheConfig.setConfigProvider(configProvider);
globalCacheConfig.setLocalCacheBuilders(localBuilders);
globalCacheConfig.setRemoteCacheBuilders(remoteBuilders);
globalCacheConfig.setStatIntervalMinutes(15);
globalCacheConfig.setAreaInCacheName(false);
return globalCacheConfig;
}
}
JetCache2.0的核心是com.alicp.jetcache.Cache
接口(以下简写为Cache
),它提供了部分类似于javax.cache.Cache
(JSR107)的API操作。没有完整实现JSR107的原因包括:
javax.cache.Cache
中定义的有些操作无法高效率的实现,比如一些原子操作方法和类似removeAll()
这样的方法。以下是Cache接口中和JSR107的javax.cache.Cache接口一致的方法,除了不会抛出异常,这些方法的签名和行为和JSR107都是一样的。
V get(K key)
void put(K key, V value);
boolean putIfAbsent(K key, V value); //多级缓存MultiLevelCache不支持此方法
boolean remove(K key);
T unwrap(Class clazz);//2.2版本前,多级缓存MultiLevelCache不支持此方法
Map getAll(Set extends K> keys);
void putAll(Map extends K,? extends V> map);
void removeAll(Set extends K> keys);
V computeIfAbsent(K key, Function loader)
当key对应的缓存不存在时,使用loader加载。通过这种方式,loader的加载时间可以被统计到。
V computeIfAbsent(K key, Function loader, boolean cacheNullWhenLoaderReturnNull)
当key对应的缓存不存在时,使用loader加载。cacheNullWhenLoaderReturnNull参数指定了当loader加载出来时null值的时候,是否要进行缓存(有时候即使是null值也是通过很繁重的查询才得到的,需要缓存)。
V computeIfAbsent(K key, Function loader, boolean cacheNullWhenLoaderReturnNull, long expire, TimeUnit timeUnit)
当key对应的缓存不存在时,使用loader加载。cacheNullWhenLoaderReturnNull参数指定了当loader加载出来时null值的时候,是否要进行缓存(有时候即使是null值也是通过很繁重的查询才得到的,需要缓存)。expire和timeUnit指定了缓存的超时时间,会覆盖缓存的默认超时时间。
void put(K key, V value, long expire, TimeUnit timeUnit)
put操作,expire和timeUnit指定了缓存的超时时间,会覆盖缓存的默认超时时间。
AutoReleaseLock tryLock(K key, long expire, TimeUnit timeUnit)
boolean tryLockAndRun(K key, long expire, TimeUnit timeUnit, Runnable action)
非堵塞的尝试获取一个锁,如果对应的key还没有锁,返回一个AutoReleaseLock,否则立即返回空。如果Cache实例是本地的,它是一个本地锁,在本JVM中有效;如果是redis等远程缓存,它是一个不十分严格的分布式锁。锁的超时时间由expire和timeUnit指定。多级缓存的情况会使用最后一级做tryLock操作。用法如下:
// 使用try-with-resource方式,可以自动释放锁
try(AutoReleaseLock lock = cache.tryLock("MyKey",100, TimeUnit.SECONDS)){
if(lock != null){
// do something
}
}
上面的代码有个潜在的坑是忘记判断if(lock!=null),所以一般可以直接用tryLockAndRun更加简单
boolean hasRun = tryLockAndRun("MyKey",100, TimeUnit.SECONDS), () -> {
// do something
};
tryLock内部会在访问远程缓存失败时重试,会自动释放,而且不会释放不属于自己的锁,比你自己做这些要简单。当然,基于远程缓存实现的任何分布式锁都不会是严格的分布式锁,不能和基于ZooKeeper或Consul做的锁相比。
Vget(K key)这样的方法虽然用起来方便,但有功能上的缺陷,当get返回null的时候,无法断定是对应的key不存在,还是访问缓存发生了异常,所以JetCache针对部分操作提供了另外一套API,提供了完整的返回值,包括:
CacheGetResult GET(K key);
MultiGetResult GET_ALL(Set extends K> keys);
CacheResult PUT(K key, V value);
CacheResult PUT(K key, V value, long expireAfterWrite, TimeUnit timeUnit);
CacheResult PUT_ALL(Map extends K, ? extends V> map);
CacheResult PUT_ALL(Map extends K, ? extends V> map, long expireAfterWrite, TimeUnit timeUnit);
CacheResult REMOVE(K key);
CacheResult REMOVE_ALL(Set extends K> keys);
CacheResult PUT_IF_ABSENT(K key, V value, long expireAfterWrite, TimeUnit timeUnit);
这些方法的特征是方法名为大写,与小写的普通方法对应,提供了完整的返回值,用起来也稍微繁琐一些。例如:
CacheGetResult r = cache.GET(orderId);
if( r.isSuccess() ){
OrderDO order = r.getValue();
} else if (r.getResultCode() == CacheResultCode.NOT_EXISTS) {
System.out.println("cache miss:" + orderId);
} else if(r.getResultCode() == CacheResultCode.EXPIRED) {
System.out.println("cache expired:" + orderId));
} else {
System.out.println("cache get error:" + orderId);
}
在Springbean中使用@CreateCache注解创建一个Cache实例。例如
@CreateCache(expire = 100)
private Cache userCache;
属性 |
默认值 |
说明 |
area |
“default” |
如果需要连接多个缓存系统,可在配置多个cache area,这个属性指定要使用的那个area的name |
name |
未定义 |
指定缓存的名称,不是必须的,如果没有指定,会使用类名+方法名。name会被用于远程缓存的key前缀。另外在统计中,一个简短有意义的名字会提高可读性。如果两个 |
expire |
未定义 |
该Cache实例的默认超时时间定义,注解上没有定义的时候会使用全局配置,如果此时全局配置也没有定义,则取无穷大 |
timeUnit |
TimeUnit.SECONDS |
指定expire的单位 |
cacheType |
CacheType.REMOTE |
缓存的类型,包括CacheType.REMOTE、CacheType.LOCAL、CacheType.BOTH。如果定义为BOTH,会使用LOCAL和REMOTE组合成两级缓存 |
localLimit |
未定义 |
如果cacheType为CacheType.LOCAL或CacheType.BOTH,这个参数指定本地缓存的最大元素数量,以控制内存占用。注解上没有定义的时候会使用全局配置,如果此时全局配置也没有定义,则取100 |
serialPolicy |
未定义 |
如果cacheType为CacheType.REMOTE或CacheType.BOTH,指定远程缓存的序列化方式。JetCache内置的可选值为SerialPolicy.JAVA和SerialPolicy.KRYO。注解上没有定义的时候会使用全局配置,如果此时全局配置也没有定义,则取SerialPolicy.JAVA |
keyConvertor |
未定义 |
指定KEY的转换方式,用于将复杂的KEY类型转换为缓存实现可以接受的类型,JetCache内置的可选值为KeyConvertor.FASTJSON和KeyConvertor.NONE。NONE表示不转换,FASTJSON通过fastjson将复杂对象KEY转换成String。如果注解上没有定义,则使用全局配置。 |
对于以上未定义默认值的参数,如果没有指定,将使用yml中指定的全局配置,请参考配置详解。
JetCache方法缓存和SpringCache比较类似,它原生提供了TTL支持,以保证最终一致,并且支持二级缓存。JetCache2.4以后支持基于注解的缓存更新和删除。
在spring环境下,使用@Cached注解可以为一个方法添加缓存,@CacheUpdate用于更新缓存,@CacheInvalidate用于移除缓存元素。注解可以加在接口上也可以加在类上,加注解的类必须是一个spring bean,例如:
public interface UserService {
@Cached(name="userCache.", key="#userId", expire = 3600)
User getUserById(long userId);
@CacheUpdate(name="userCache.", key="#user.userId", value="#user")
void updateUser(User user);
@CacheInvalidate(name="userCache.", key="#userId")
void deleteUser(long userId);
}
key使用Spring的SpEL脚本来指定。如果要使用参数名(比如这里的key="#userId"),项目编译设置target必须为1.8格式,并且指定javac的-parameters参数,否则就要使用key="args[0]"这样按下标访问的形式。
@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 |
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的时候才进行缓存 |
@CacheInvalidate注解说明:
属性 |
默认值 |
说明 |
area |
“default” |
如果在配置中配置了多个缓存area,在这里指定使用哪个area,指向对应的@Cached定义。 |
name |
未定义 |
指定缓存的唯一名称,指向对应的@Cached定义。 |
key |
未定义 |
使用SpEL指定key |
condition |
未定义 |
使用SpEL指定条件,如果表达式返回true才执行删除 |
@CacheUpdate注解说明:
属性 |
默认值 |
说明 |
area |
“default” |
如果在配置中配置了多个缓存area,在这里指定使用哪个area,指向对应的@Cached定义。 |
name |
未定义 |
指定缓存的唯一名称,指向对应的@Cached定义。 |
key |
未定义 |
使用SpEL指定key |
value |
未定义 |
使用SpEL指定value |
condition |
未定义 |
使用SpEL指定条件,如果表达式返回true才执行更新 |
使用@CacheUpdate和@CacheInvalidate的时候,相关的缓存操作可能会失败(比如网络IO错误),所以指定缓存的超时时间是非常重要的。
@CacheRefresh注解说明:
属性 |
默认值 |
说明 |
refresh |
未定义 |
刷新间隔 |
timeUnit |
TimeUnit.SECONDS |
时间单位 |
stopRefreshAfterLastAccess |
未定义 |
指定该key多长时间没有访问就停止刷新,如果不指定会一直刷新 |
refreshLockTimeout |
60秒 |
类型为BOTH/REMOTE的缓存刷新时,同时只会有一台服务器在刷新,这台服务器会在远程缓存放置一个分布式锁,此配置指定该锁的超时时间 |
对于以上未定义默认值的参数,如果没有指定,将使用yml中指定的全局配置,全局配置请参考配置说明。
yml配置文件案例(如果没使用springboot,直接配置GlobalCacheConfig是类似的,参考快速入门教程):
jetcache:
statIntervalMinutes: 15
areaInCacheName: false
hiddenPackages: com.alibaba
local:
default:
type: caffeine
limit: 100
keyConvertor: fastjson
expireAfterWriteInMillis: 100000
otherArea:
type: linkedhashmap
limit: 100
keyConvertor: none
expireAfterWriteInMillis: 100000
remote:
default:
type: redis
keyConvertor: fastjson
valueEncoder: java
valueDecoder: java
poolConfig:
minIdle: 5
maxIdle: 20
maxTotal: 50
host: ${redis.host}
port: ${redis.port}
otherArea:
type: redis
keyConvertor: fastjson
valueEncoder: kryo
valueDecoder: kryo
poolConfig:
minIdle: 5
maxIdle: 20
maxTotal: 50
host: ${redis.host}
port: ${redis.port}
配置通用说明如下
属性 |
默认值 |
说明 |
jetcache.statIntervalMinutes |
0 |
统计间隔,0表示不统计 |
jetcache.areaInCacheName |
true |
jetcache-anno把cacheName作为远程缓存key前缀,2.4.3以前的版本总是把areaName加在cacheName中,因此areaName也出现在key前缀中。2.4.4以后可以配置,为了保持远程key兼容默认值为true,但是新项目的话false更合理些。 |
jetcache.hiddenPackages |
无 |
@Cached和@CreateCache自动生成name的时候,为了不让name太长,hiddenPackages指定的包名前缀被截掉 |
jetcache.[local|remote].${area}.type |
无 |
缓存类型。tair、redis为当前支持的远程缓存;linkedhashmap、caffeine为当前支持的本地缓存类型 |
jetcache.[local|remote].${area}.keyConvertor |
无 |
key转换器的全局配置,当前只有一个已经实现的keyConvertor:fastjson。仅当使用@CreateCache且缓存类型为LOCAL时可以指定为none,此时通过equals方法来识别key。方法缓存必须指定keyConvertor |
jetcache.[local|remote].${area}.valueEncoder |
java |
序列化器的全局配置。仅remote类型的缓存需要指定,可选java和kryo |
jetcache.[local|remote].${area}.valueDecoder |
java |
序列化器的全局配置。仅remote类型的缓存需要指定,可选java和kryo |
jetcache.[local|remote].${area}.limit |
100 |
每个缓存实例的最大元素的全局配置,仅local类型的缓存需要指定。注意是每个缓存实例的限制,而不是全部,比如这里指定100,然后用@CreateCache创建了两个缓存实例(并且注解上没有设置localLimit属性),那么每个缓存实例的限制都是100 |
jetcache.[local|remote].${area}.expireAfterWriteInMillis |
无穷大 |
以毫秒为单位指定超时时间的全局配置(以前为defaultExpireInMillis) |
jetcache.local.${area}.expireAfterAccessInMillis |
0 |
需要jetcache2.2以上,以毫秒为单位,指定多长时间没有访问,就让缓存失效,当前只有本地缓存支持。0表示不使用这个功能。 |
上表中${area}对应@Cached和@CreateCache的area属性。注意如果注解上没有指定area,默认值是"default"。
关于缓存的超时时间,有多个地方指定,澄清说明一下:
CacheBuilder提供使用代码直接构造Cache实例的方式,使用说明看这里。如果没有使用Spring,可以使用CacheBuilder,否则没有必要直接使用CacheBuilder。
从JetCache2.2版本开始,所有的大写API返回的CacheResult都支持异步。当底层的缓存实现支持异步的时候,大写API返回的结果都是异步的。当前支持异步的实现只有jetcache的redis-luttece实现,其他的缓存实现(内存中的、Tair、Jedis等),所有的异步接口都会同步堵塞,这样API仍然是兼容的。
以下的例子假设使用redis-luttece访问cache,例如:
CacheGetResult r = cache.GET(userId);
这一行代码执行完以后,缓存操作可能还没有完成,如果此时调用r.isSuccess()或者r.getValue()或者r.getMessage()将会堵塞直到缓存操作完成。如果不想被堵塞,并且需要在缓存操作完成以后执行后续操作,可以这样做:
CompletionStage future = r.future();
future.thenRun(() -> {
if(r.isSuccess()){
System.out.println(r.getValue());
}
});
以上代码将会在缓存操作异步完成后,在完成异步操作的线程中调用thenRun中指定的回调。CompletionStage是Java8新增的功能,如果对此不太熟悉可以先查阅相关的文档。需要注意的是,既然已经选择了异步的开发方式,在回调中不能调用堵塞方法,以免堵塞其他的线程(回调方法很可能是在event loop线程中执行的)。
部分小写的api不需要任何修改,就可以直接享受到异步开发的好处。比如put和removeAll方法,由于它们没有返回值,所以此时就直接优化成异步调用,能够减少RT;而get方法由于需要取返回值,所以仍然会堵塞。
LoadingCache类提供了自动load的功能,它是一个包装,基于decorator模式,也实现了Cache接口。如果CacheBuilder指定了loader,那么buildCache返回的Cache实例就是经过LoadingCache包装过的。例如:
Cache userCache = LinkedHashMapCacheBuilder.createLinkedHashMapCacheBuilder()
.loader(key -> loadUserFromDatabase(key))
.buildCache();
LoadingCache的get和getAll方法,在缓存未命中的情况下,会调用loader,如果loader抛出一场,get和getAll会抛出CacheInvokeException。
需要注意
注解的属性只能是常量,所以没有办法在CreateCache注解中指定loader,不过我们可以这样:
@CreateCache
private Cache userCache;
@PostConstruct
public void init(){
userCache.config().setLoader(this::loadUserFromDatabase);
}
@CreateCache总是初始化一个经过LoadingCache包装的Cache,直接在config中设置loader,可以实时生效。
从JetCache2.2版本开始,RefreshCache基于decorator模式提供了自动刷新的缓存的能力,目的是为了防止缓存失效时造成的雪崩效应打爆数据库。同时设置了loader和refreshPolicy的时候,CacheBuilder的buildCache方法返回的Cache实例经过了RefreshCache的包装。
RefreshPolicy policy = RefreshPolicy.newPolicy(1, TimeUnit.MINUTES)
.stopRefreshAfterLastAccess(30, TimeUnit.MINUTES);
Cache orderSumCache = LinkedHashMapCacheBuilder
.createLinkedHashMapCacheBuilder()
.loader(key -> loadOrderSumFromDatabase(key))
.refreshPolicy(policy)
.buildCache();
对一些key比较少,实时性要求不高,加载开销非常大的缓存场景,适合使用自动刷新。上面的代码指定每分钟刷新一次,30分钟如果没有访问就停止刷新。如果缓存是redis或者多级缓存最后一级是redis,缓存加载行为是全局唯一的,也就是说不管有多少台服务器,同时只有一个服务器在刷新,这是通过tryLock实现的,目的是为了降低后端的加载负担。
与LoadingCache一样,使用@CreateCache时,我们需要这样来添加自动刷新功能
@CreateCache
private Cache orderSumCache;
@PostConstruct
public void init(){
RefreshPolicy policy = RefreshPolicy.newPolicy(1, TimeUnit.MINUTES)
.stopRefreshAfterLastAccess(30, TimeUnit.MINUTES);
orderSumCache.config().setLoader(this::loadOrderSumFromDatabase);
orderSumCache.config().setRefreshPolicy(policy);
}
redis有多种java版本的客户端,JetCache2.2以前使用jedis客户端访问redis。从JetCache2.2版本开始,增加了对luttece客户端的支持,jetcache的luttece支持提供了异步操作和redis集群支持。
如果选用jedis访问redis,对应的maven artifact是jetcache-redis和jetcache-starter-redis(spring boot)。
application.yml文件如下(这里省去了local相关的配置):
jetcache:
areaInCacheName: false
remote:
default:
type: redis
keyConvertor: fastjson
poolConfig:
minIdle: 5
maxIdle: 20
maxTotal: 50
host: ${redis.host}
port: ${redis.port}
#sentinels: 127.0.0.1:26379 , 127.0.0.1:26380, 127.0.0.1:26381
#masterName: mymaster
如果需要直接操作JedisPool,可以通过以下方式获取
@Bean(name = "defaultPool")
@DependsOn(RedisAutoConfiguration.AUTO_INIT_BEAN_NAME)//jetcache2.2+
//@DependsOn("redisAutoInit")//jetcache2.1
public JedisPoolFactory defaultPool() {
return new JedisPoolFactory("remote.default", JedisPool.class);
}
然后可以直接使用
@Autowired
private Pool defaultPool;
也可以用Cache
接口上的
方法来获取JedisPool,参见RedisCache.unwrap源代码。
@Configuration
@EnableMethodCache(basePackages = "com.company.mypackage")
@EnableCreateCacheAnnotation
public class JetCacheConfig {
@Bean
public Pool pool(){
// build jedis pool ...
}
@Bean
public SpringConfigProvider springConfigProvider() {
return new SpringConfigProvider();
}
@Bean
public GlobalCacheConfig config(SpringConfigProvider configProvider, Pool pool){
Map localBuilders = new HashMap();
EmbeddedCacheBuilder localBuilder = LinkedHashMapCacheBuilder
.createLinkedHashMapCacheBuilder()
.keyConvertor(FastjsonKeyConvertor.INSTANCE);
localBuilders.put(CacheConsts.DEFAULT_AREA, localBuilder);
Map remoteBuilders = new HashMap();
RedisCacheBuilder remoteCacheBuilder = RedisCacheBuilder.createRedisCacheBuilder()
.keyConvertor(FastjsonKeyConvertor.INSTANCE)
.valueEncoder(JavaValueEncoder.INSTANCE)
.valueDecoder(JavaValueDecoder.INSTANCE)
.jedisPool(pool);
remoteBuilders.put(CacheConsts.DEFAULT_AREA, remoteCacheBuilder);
GlobalCacheConfig globalCacheConfig = new GlobalCacheConfig();
globalCacheConfig.setConfigProvider(configProvider);
globalCacheConfig.setLocalCacheBuilders(localBuilders);
globalCacheConfig.setRemoteCacheBuilders(remoteBuilders);
globalCacheConfig.setStatIntervalMinutes(15);
globalCacheConfig.setAreaInCacheName(false);
return globalCacheConfig;
}
}
如果不通过@CreateCache和@Cached注解,可以通过下面的方式创建RedisCache。通过注解创建的缓存会自动设置keyPrefix,这里是手工创建缓存,对于远程缓存需要设置keyPrefix属性,以免不同Cache实例的key发生冲突。
GenericObjectPoolConfig pc = new GenericObjectPoolConfig();
pc.setMinIdle(2);
pc.setMaxIdle(10);
pc.setMaxTotal(10);
JedisPool pool = new JedisPool(pc, "localhost", 6379);
Cache orderCache = RedisCacheBuilder.createRedisCacheBuilder()
.keyConvertor(FastjsonKeyConvertor.INSTANCE)
.valueEncoder(JavaValueEncoder.INSTANCE)
.valueDecoder(JavaValueDecoder.INSTANCE)
.jedisPool(pool)
.keyPrefix("orderCache")
.expireAfterWrite(200, TimeUnit.SECONDS)
.buildCache();
如果遇到这个错误
java.lang.NoSuchMethodError: redis.clients.jedis.JedisPool.(Lorg/apache/commons/pool2/impl/GenericObjectPoolConfig;Ljava/lang/String;IILjava/lang/String;ILjava/lang/String;Z)V
请确保jedis的版本在2.9.0以上,spring boot 1.5以下版本的spring-boot-dependencies会引入较低版本的jedis,可以在自己的pom中强制直接依赖jedis版本2.9.0:
redis.clients
jedis
2.9.0
redis有多种java版本的客户端,JetCache2.2以前使用jedis客户端访问redis。从JetCache2.2版本开始,增加了对lettuce客户端的支持,JetCache的lettuce支持提供了异步操作和redis集群支持。
使用lettuce访问redis,对应的maven artifact是jetcache-redis-lettuce和jetcache-starter-redis-lettuce。lettuce使用Netty建立单个连接连redis,所以不需要配置连接池。
注意:新发布的lettuce5更换了groupId和包名,2.3版本的JetCache同时支持lettuce4和5,jetcache-redis-lettuce,jetcache-starter-redis-lettuce提供lettuce5支持,jetcache-redis-lettuce4和jetcache-starter-redis-lettuce4提供lettuce4支持。
注意:JetCache2.2版本中,lettuce单词存在错误的拼写,错写为“luttece”,该错误存在于包名、类名和配置中,2.3已经改正。
application.yml文件如下(这里省去了local相关的配置):
jetcache:
areaInCacheName: false
remote:
default:
type: redis.lettuce
keyConvertor: fastjson
uri: redis://127.0.0.1:6379/
如果使用sentinel做自动主备切换,uri可以配置为redis-sentinel://127.0.0.1:26379,127.0.0.1:26380,127.0.0.1:26381/?sentinelMasterId=mymaster
如果是集群:
jetcache:
areaInCacheName: false
remote:
default:
type: redis.lettuce
keyConvertor: fastjson
uri:
- redis://127.0.0.1:7000
- redis://127.0.0.1:7001
- redis://127.0.0.1:7002
如果需要直接使用lettuce的RedisClient:
@Bean(name = "defaultClient")
@DependsOn(RedisLettuceAutoConfiguration.AUTO_INIT_BEAN_NAME)
public LettuceFactory defaultClient() {
return new LettuceFactory("remote.default", RedisClient.class);
}
然后可以直接使用
@Autowired
private RedisClient defaultClient;
也可以用Cache
接口上的
方法来获取RedisClient
和RedisCommands
等。参考RedisLettuceCache.unwrap源代码。
@Configuration
@EnableMethodCache(basePackages = "com.company.mypackage")
@EnableCreateCacheAnnotation
public class JetCacheConfig {
@Bean
public RedisClient redisClient(){
RedisClient client = RedisClient.create("redis://127.0.0.1");
return client;
}
@Bean
public SpringConfigProvider springConfigProvider() {
return new SpringConfigProvider();
}
@Bean
public GlobalCacheConfig config(SpringConfigProvider configProvider,RedisClient redisClient){
Map localBuilders = new HashMap();
EmbeddedCacheBuilder localBuilder = LinkedHashMapCacheBuilder
.createLinkedHashMapCacheBuilder()
.keyConvertor(FastjsonKeyConvertor.INSTANCE);
localBuilders.put(CacheConsts.DEFAULT_AREA, localBuilder);
Map remoteBuilders = new HashMap();
RedisLettuceCacheBuilder remoteCacheBuilder = RedisLettuceCacheBuilder.createRedisLettuceCacheBuilder()
.keyConvertor(FastjsonKeyConvertor.INSTANCE)
.valueEncoder(JavaValueEncoder.INSTANCE)
.valueDecoder(JavaValueDecoder.INSTANCE)
.redisClient(redisClient);
remoteBuilders.put(CacheConsts.DEFAULT_AREA, remoteCacheBuilder);
GlobalCacheConfig globalCacheConfig = new GlobalCacheConfig();
globalCacheConfig.setConfigProvider(configProvider);
globalCacheConfig.setLocalCacheBuilders(localBuilders);
globalCacheConfig.setRemoteCacheBuilders(remoteBuilders);
globalCacheConfig.setStatIntervalMinutes(15);
globalCacheConfig.setAreaInCacheName(false);
return globalCacheConfig;
}
}
如果不通过@CreateCache和@Cached注解,可以通过下面的方式创建Cache。通过注解创建的缓存会自动设置keyPrefix,这里是手工创建缓存,对于远程缓存需要设置keyPrefix属性,以免不同Cache实例的key发生冲突。
RedisClient client = RedisClient.create("redis://127.0.0.1");
Cache orderCache = RedisLettuceCacheBuilder.createRedisLettuceCacheBuilder()
.keyConvertor(FastjsonKeyConvertor.INSTANCE)
.valueEncoder(JavaValueEncoder.INSTANCE)
.valueDecoder(JavaValueDecoder.INSTANCE)
.redisClient(client)
.keyPrefix("orderCache")
.expireAfterWrite(200, TimeUnit.SECONDS)
.buildCache();
本地缓存当前有两个实现。如果自己用jetcache-core的Cache API,可以不指定keyConvertor,此时本地缓存使用equals方法来比较key。如果使用jetcache-anno中的@Cached、@CreateCache等注解,必须指定keyConvertor。
LinkedHashMapCache是JetCache中实现的一个最简单的Cache,使用LinkedHashMap做LRU方式淘汰。
Cache cache = LinkedHashMapCacheBuilder.createLinkedHashMapCacheBuilder()
.limit(100)
.expireAfterWrite(200, TimeUnit.SECONDS)
.buildCache();
caffeine cache的介绍看这里,它是guava cache的后续作品。
Cache cache = CaffeineCacheBuilder.createCaffeineCacheBuilder()
.limit(100)
.expireAfterWrite(200, TimeUnit.SECONDS)
.buildCache();
当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
default_ChannelManager.getChannelById | 6.65| 98.75%| 399| 394| 0| 4| 0.0| 0
default_ConfigManager.getChannelConfig | 1.97| 96.61%| 118| 114| 0| 4| 0.0| 0
default_ConfigManager.getGameConfig | 0.00| 0.00%| 0| 0| 0| 0| 0.0| 0
default_ConfigManager.getInstanceConfig | 43.98| 99.96%| 2,639| 2,638| 0| 0| 0.0| 0
default_ConfigManager.getInstanceConfigSettingMap | 2.45| 70.75%| 147| 104| 0| 43| 0.0| 0
default_GameManager.getGameById | 1.33|100.00%| 80| 80| 0| 0| 0.0| 0
default_GameManager.getGameUrlByUrlKey | 7.33|100.00%| 440| 440| 0| 0| 0.0| 0
default_InstanceManager.getInstanceById | 30.98| 99.52%| 1,859| 1,850| 0| 0| 0.0| 0
default_InstanceManager.getInstanceById_local | 30.98| 96.40%| 1,859| 1,792| 0| 67| 0.0| 0
default_InstanceManager.getInstanceById_remote | 1.12| 86.57%| 67| 58| 0| 6| 0.0| 0
default_IssueDao.getIssueById | 7.62| 81.40%| 457| 372| 0| 63| 0.0| 0
default_IssueDao.getRecentOnSaleIssues | 8.00| 85.21%| 480| 409| 0| 71| 0.0| 0
default_IssueDao.getRecentOpenAwardIssues | 2.52| 82.78%| 151| 125| 0| 26| 0.0| 0
default_PrizeManager.getPrizeMap | 0.82|100.00%| 49| 49| 0| 0| 0.0| 0
default_TopicManager.getOnSaleTopics | 0.97|100.00%| 58| 58| 0| 0| 0.0| 0
default_TopicManager.getOnSaleTopics_local | 0.97| 91.38%| 58| 53| 0| 5| 0.0| 0
default_TopicManager.getOnSaleTopics_remote | 0.08|100.00%| 5| 5| 0| 0| 0.0| 0
default_TopicManager.getTopicByTopicId | 2.90| 98.85%| 174| 172| 0| 0| 0.0| 0
default_TopicManager.getTopicByTopicId_local | 2.90| 96.55%| 174| 168| 0| 6| 0.0| 0
default_TopicManager.getTopicByTopicId_remote | 0.10| 66.67%| 6| 4| 0| 2| 0.0| 0
default_TopicManager.getTopicList | 0.02|100.00%| 1| 1| 0| 0| 0.0| 0
default_TopicManager.getTopicList_local | 0.02| 0.00%| 1| 0| 0| 1| 0.0| 0
default_TopicManager.getTopicList_remote | 0.02|100.00%| 1| 1| 0| 0| 0.0| 0
-----------------------------------------------------+----------+-------+--------------+--------------+--------------+--------------+-----------+-----------
只有使用computeIfAbsent方法或者@Cached注解才会统计loadTime。用get方法取缓存,没有命中的话自己去数据库load,显然是无法统计到的。
如果需要定制输出,可以这样做:
@Bean
public SpringConfigProvider springConfigProvider() {
return new SpringConfigProvider(){
public Consumer statCallback() {
// return new StatInfoLogger(false);
... // 实现自己的logger
}
};
}
JetCache按statIntervalMinutes指定的周期,定期调用statCallback返回着这个Consumer,传入的StatInfo是已经统计好的数据。这个方法默认的实现是:
returnnew StatInfoLogger(false);
StatInfoLogger的构造参数设置为true会有更详细的统计信息,包括put等操作的统计。StatInfoLogger输出的是给人读的信息,你也可以自定义logger将日志输出成特定格式,然后通过日志系统统一收集和统计。
如果想要让jetcache的日志输出到独立的文件中,在使用logback的情况下可以这样配置:
jetcache.log
jetcache.log.%d{yyyy-MM-dd}
30
%-4relative [%thread] %-5level %logger{35} - %msg%n
JetCache2版本的@Cached和@CreateCache等注解都是基于Spring4.X版本实现的,在没有Spring支持的情况下,注解将不能使用。但是可以直接使用JetCache的API来创建、管理、监控Cache,多级缓存也可以使用。
创建缓存的操作类似guava/caffeinecache,例如下面的代码创建基于内存的LinkedHashMapCache:
Cache cache = LinkedHashMapCacheBuilder.createLinkedHashMapCacheBuilder()
.limit(100)
.expireAfterWrite(200, TimeUnit.SECONDS)
.buildCache();
创建RedisCache:
GenericObjectPoolConfig pc = new GenericObjectPoolConfig();
pc.setMinIdle(2);
pc.setMaxIdle(10);
pc.setMaxTotal(10);
JedisPool pool = new JedisPool(pc, "localhost", 6379);
Cache orderCache = RedisCacheBuilder.createRedisCacheBuilder()
.keyConvertor(FastjsonKeyConvertor.INSTANCE)
.valueEncoder(JavaValueEncoder.INSTANCE)
.valueDecoder(JavaValueDecoder.INSTANCE)
.jedisPool(pool)
.keyPrefix("orderCache")
.expireAfterWrite(200, TimeUnit.SECONDS)
.buildCache();
在2.2以后通过下面的方式创建多级缓存:
Cache multiLevelCache = MultiLevelCacheBuilder.createMultiLevelCacheBuilder()
.addCache(memoryCache, redisCache)
.expireAfterWrite(100, TimeUnit.SECONDS)
.buildCache();
实际上,使用MultiLevelCache可以创建多级缓存,它的构造函数接收的是一个Cache数组(可变参数)。
如果是2.2之前的版本:
Cache memoryCache = ...
Cache redisCache = ...
Cache multiLevelCache = new MultiLevelCache(memoryCache, redisCache);
如果要对Cache进行监控统计:
Cache orderCache = ...
CacheMonitor orderCacheMonitor = new DefaultCacheMonitor("OrderCache");
orderCache.config().getMonitors().add(orderCacheMonitor); // jetcache 2.2+, or call builder.addMonitor() before buildCache()
// Cache monitedOrderCache = new MonitoredCache(orderCache, orderCacheMonitor); //before jetcache 2.2
int resetTime = 1;
boolean verboseLog = false;
DefaultCacheMonitorManager cacheMonitorManager = new DefaultCacheMonitorManager(resetTime, TimeUnit.SECONDS, verboseLog);
cacheMonitorManager.add(orderCacheMonitor);
cacheMonitorManager.start();
首先创建一个CacheMonitor,每个DefaultCacheMonitor只能用于一个Cache。当DefaultCacheMonitorManager启动以后,会使用slf4j按指定的时间定期输出统计信息到日志中(简版输出格式参见统计),DefaultCacheMonitor构造时指定的名字会作为输出时cache的名字。
在组装多级缓存的过程中,可以给每个缓存安装一个Monitor,这样可以监控每一级的命中情况。
也可以自己对统计信息进行处理,调用下面的构造方法创建DefaultCacheMonitorManager:
public DefaultCacheMonitorManager(int resetTime, TimeUnit resetTimeUnit, Consumer stat
clone下来以后,可以按maven项目导入idea或eclipse。
跑通单元测试,需要在本地运行redis,先安装docker,然后用下面的命令运行redis-sentinel
docker run --rm -it -p 6379-6381:6379-6381 -p 26379-26381:26379-26381 areyouok/redis-sentinel
接下来mvn cleantest可以跑通所有测试,如果在IDE里面,可能还需要给javac设置-parameters参数。需要注意的是机器繁忙时单元测试有可能会失败,因为很多单元测试使用了sleep,为了不让单元测试运行的时间过长,sleep的时间都设置的比较短,这样机器卡顿时可能导致检查失败,不过对于正常机器这并不经常发生。
使用snapshot版本,在自己的pom里面加上:
sonatype-nexus-snapshots
Sonatype Nexus Snapshots
https://oss.sonatype.org/content/repositories/snapshots
false
true