JetCache的使用

JetCache是一个基于Java的缓存系统封装, 提供统一的API和注解来简化缓存的使用, 可以支持:

  1. TTL
  2. 两级缓存
  3. 分布式自动刷新
  4. 通过注解创建并配置缓存
  5. 缓存使用的统计
  6. Key生成策略与Value序列化方式自定义
  7. 异步CacheAPI
  8. Spring Boot支持

要求

  1. JDK1.8
  2. 可选Spring Framework 4.0.8+
  3. 可选Spring Boot 1.1.9+

基本使用

仅仅介绍 SpringBoot 下的使用

快速开始

1
2
3
4
5
<dependency>
<groupId>com.alicp.jetcache</groupId>
<artifactId>jetcache-starter-redis</artifactId>
<version>2.5.14</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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
1
2
3
4
5
6
7
8
@SpringBootApplication
@EnableMethodCache(basePackages = "com.company.mypackage")
@EnableCreateCacheAnnotation
public class MySpringBootApp {
public static void main(String[] args) {
SpringApplication.run(MySpringBootApp.class);
}
}

创建缓存实例的方式

一级缓存

1
2
@CreateCache(expire = 100)
private Cache<Long, UserDO> userCache;

二级缓存

1
2
@CreateCache(name = "UserService.userCache", expire = 100, cacheType = CacheType.BOTH, localLimit = 50)
private Cache<Long, UserDO> userCache;

创建方法缓存的方式

1
2
3
4
public interface UserService {
@Cached(name="UserService.getUserById", expire = 3600)
User getUserById(long userId);
}

CacheAPI

缓存的写入与读取都通过这个进行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

基本api如下:

```java
// 获取
V get(K key)
// 插入
void put(K key, V value);
// 指定过期时间
void put(K key, V value, long expire, TimeUnit timeUnit)
boolean putIfAbsent(K key, V value); //多级缓存MultiLevelCache不支持此方法
boolean remove(K key);
<T> T unwrap(Class<T> clazz);//2.2版本前,多级缓存MultiLevelCache不支持此方法
Map<K,V> getAll(Set<? extends K> keys);
void putAll(Map<? extends K,? extends V> map);
void removeAll(Set<? extends K> keys);
// 当key对应的缓存不存在时,使用loader加载。
// cacheNullWhenLoaderReturnNull参数指定了当loader加载出来时null值的时候,是否要进行缓存(有时候即使是null值也是通过很繁重的查询才得到的,需要缓存)。
// expire和timeUnit指定了缓存的超时时间,会覆盖缓存的默认超时时间。
V computeIfAbsent(K key, Function<K, V> loader)
V computeIfAbsent(K key, Function<K, V> loader, boolean cacheNullWhenLoaderReturnNull)
V computeIfAbsent(K key, Function<K, V> loader, boolean cacheNullWhenLoaderReturnNull, long expire, TimeUnit timeUnit)
// 尝试获取锁
AutoReleaseLock tryLock(K key, long expire, TimeUnit timeUnit) //可能获取到AutoReleaseLock为null,需要自行判断
boolean tryLockAndRun(K key, long expire, TimeUnit timeUnit, Runnable action)

特殊的API

jetCache提供了大写的API, 与小写的区别是返回一个CacheResult对象, 例如:可以通过大写的GET判断获取到的key不存在还是发生了异常情况导致没有获取到,还是缓存过期导致没有获取到.

1
2
3
4
5
6
7
8
9
CacheGetResult<V> GET(K key);
MultiGetResult<K, V> 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);

使用上要这样

1
2
3
4
5
6
7
8
9
10
CacheGetResult<OrderDO> 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);
}

注解的使用

成员变量上的注解CreateCache

有如下参数:

属性 默认值 说明
area “default” 如果需要连接多个缓存系统,可在配置多个cache area,这个属性指定要使用的那个area的name
name 未定义 指定缓存的名称,不是必须的,如果没有指定,会使用类名+方法名。name会被用于远程缓存的key前缀。另外在统计中,一个简短有意义的名字会提高可读性。如果两个@CreateCachenamearea相同,它们会指向同一个Cache实例
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。如果注解上没有定义,则使用全局配置。

方法上的注解Cached,CacheUpdate,CacheInvalidate,CacheRefresh,CachePenetrationProtect

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
CacheUpdate
属性 默认值 说明
area “default” 如果在配置中配置了多个缓存area,在这里指定使用哪个area,指向对应的@Cached定义。
name 未定义 指定缓存的唯一名称,指向对应的@Cached定义。
key 未定义 使用SpEL指定key
value 未定义 使用SpEL指定value
condition 未定义 使用SpEL指定条件,如果表达式返回true才执行更新,可访问方法结果#result
CacheInvalidate
属性 默认值 说明
area “default” 如果在配置中配置了多个缓存area,在这里指定使用哪个area,指向对应的@Cached定义。
name 未定义 指定缓存的唯一名称,指向对应的@Cached定义。
key 未定义 使用SpEL指定key
condition 未定义 使用SpEL指定条件,如果表达式返回true才执行删除,可访问方法结果#result
CacheRefresh
属性 默认值 说明
refresh 未定义 刷新间隔
timeUnit TimeUnit.SECONDS 时间单位
stopRefreshAfterLastAccess 未定义 指定该key多长时间没有访问就停止刷新,如果不指定会一直刷新
refreshLockTimeout 60秒 类型为BOTH/REMOTE的缓存刷新时,同时只会有一台服务器在刷新,这台服务器会在远程缓存放置一个分布式锁,此配置指定该锁的超时时间
CachePenetrationProtect

放穿透机制

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

配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
jetcache:
statIntervalMinutes: 15
areaInCacheName: false
hidePackages: 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”。

多个地方能配置过期时间,优先级为: 方法传参>注解参数>全局参数

高级功能

异步API

大写API会返回CacheGetResult对象,CacheGetResult对象的future()方法可以返回一个CompletionStage,能支持异步调用某些逻辑.

1
2
3
4
5
6
7
CacheGetResult<UserDO> r = cache.GET(userId);
CompletionStage<ResultData> future = r.future();
future.thenRun(() -> {
if(r.isSuccess()){
System.out.println(r.getValue());
}
});

无返回值的操作使用异步调用,能减少RT

自动load

Cache的get方法有时候会出现没有命中的情况, 可以设置一个loader, 没有命中时自动的加载.

1
2
3
4
// loadUserFromDatabase
Cache<Long,UserDO> userCache = LinkedHashMapCacheBuilder.createLinkedHashMapCacheBuilder()
.loader(key -> loadUserFromDatabase(key))
.buildCache();

如果loader抛出异常,get和getAll会抛出CacheInvokeException。

大写GET,GET_ALL不调用loader; 使用多级缓存时, loader不要设置到子Cache中, 应该是父Cache(MultiLevelCache)上

@CreateCache情况下使用, 例子如下

1
2
3
4
5
6
7
@CreateCache
private Cache<Long,UserDO> userCache;

@PostConstruct
public void init(){
userCache.config().setLoader(this::loadUserFromDatabase);
}

自动刷新缓存

能设置刷新策略, 防止缓存过期.

@CreateCache情况下使用, 例子如下

1
2
3
4
5
6
7
8
9
10
@CreateCache
private Cache<String, Long> 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);
}

远程缓存

Jedis

jetcache-starter-redis

配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
jetcache: 
areaInCacheName: false
remote:
default:
type: redis
keyConvertor: fastjson
poolConfig:
minIdle: 5
maxIdle: 20
maxTotal: 50
host: ${redis.host}
port: ${redis.port}
#password:***
#sentinels: 127.0.0.1:26379 , 127.0.0.1:26380, 127.0.0.1:26381
#masterName: mymaster

如果需要直接操作JedisPool,可以通过以下方式获取

1
2
3
4
5
@Bean(name = "defaultPool")
@DependsOn(RedisAutoConfiguration.AUTO_INIT_BEAN_NAME)
public JedisPoolFactory defaultPool() {
return new JedisPoolFactory("remote.default", JedisPool.class);
}

然后可以直接使用

1
2
@Autowired
private Pool<Jedis> defaultPool;

Lettuce

jetcache-starter-redis-lettuce

配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
jetcache: 
areaInCacheName: false
remote:
default:
type: redis.lettuce
keyConvertor: fastjson
uri: redis://127.0.0.1:6379/
#uri: redis-sentinel://127.0.0.1:26379,127.0.0.1:26380,127.0.0.1:26381/?sentinelMasterId=mymaster
#readFrom: slavePreferred
# 集群使用如下
jetcache:
areaInCacheName: false
remote:
default:
type: redis.lettuce
keyConvertor: fastjson
#readFrom: slavePreferred
uri:
- redis://127.0.0.1:7000
- redis://127.0.0.1:7001
- redis://127.0.0.1:7002

如果需要直接使用lettuce的RedisClient:

1
2
3
4
5
@Bean(name = "defaultClient")
@DependsOn(RedisLettuceAutoConfiguration.AUTO_INIT_BEAN_NAME)
public LettuceFactory defaultClient() {
return new LettuceFactory("remote.default", RedisClient.class);
}

然后可以直接使用

1
2
@Autowired
private RedisClient defaultClient;

本地缓存

LinkedHashMapCache

LinkedHashMapCache是JetCache中实现的一个最简单的Cache,使用LinkedHashMap做LRU方式淘汰。

1
2
3
4
Cache<Long, OrderDO> cache = LinkedHashMapCacheBuilder.createLinkedHashMapCacheBuilder()
.limit(100)
.expireAfterWrite(200, TimeUnit.SECONDS)
.buildCache();

CaffeineCache

caffeine cache的介绍看这里,它是guava cache的后续作品。

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

缓存的统计与监控

jetcache.statIntervalMinutes大于0时, jetCache会自动在日志上打印缓存的使用情况.

1
2
3
4
5
6
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
.....
-----------------------------------------------------+----------+-------+--------------+--------------+--------------+--------------+-----------+-----------

只有使用computeIfAbsent方法或者@Cached注解才会统计loadTime。用get方法取缓存,没有命中的话自己去数据库load,显然是无法统计到的。

也可以自定义输出

1
2
3
4
5
6
7
8
9
// for 2.6+
@Bean
public Consumer<StatInfo> metricsCallback() {
public Consumer<StatInfo> statCallback() {
// return new StatInfoLogger(false);
... // 实现自己的logger
}
}

1
2
3
4
5
6
7
8
9
10
11
// for 2.5
@Bean
public SpringConfigProvider springConfigProvider() {
return new SpringConfigProvider(){
public Consumer<StatInfo> statCallback() {
// return new StatInfoLogger(false);
... // 实现自己的logger
}
};
}

可以输出成单独的log文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<appender name="JETCACHE_LOGFILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>jetcache.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>jetcache.log.%d{yyyy-MM-dd}</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>

<encoder>
<pattern>%-4relative [%thread] %-5level %logger{35} - %msg%n</pattern>
</encoder>
</appender>

<logger name="com.alicp.jetcache" level="INFO" additivity="false">
<appender-ref ref="JETCACHE_LOGFILE" />
</logger>

不使用注解

不使用注解来创建缓存实例要利用各种Builder

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 本地
Cache<String, Integer> cache = LinkedHashMapCacheBuilder.createLinkedHashMapCacheBuilder()
.limit(100)
.expireAfterWrite(200, TimeUnit.SECONDS)
.buildCache();
// redis
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(FastjsonKeyConvertor.INSTANCE)
.valueEncoder(JavaValueEncoder.INSTANCE)
.valueDecoder(JavaValueDecoder.INSTANCE)
.jedisPool(pool)
.keyPrefix("orderCache")
.expireAfterWrite(200, TimeUnit.SECONDS)
.buildCache();
// 多级
Cache multiLevelCache = MultiLevelCacheBuilder.createMultiLevelCacheBuilder()
.addCache(memoryCache, redisCache)
.expireAfterWrite(100, TimeUnit.SECONDS)
.buildCache();

最后

注意

  1. key、condition等表达式中使用参数名需要javac命令的-parameters支持
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <plugins>
    <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.7.0</version>
    <configuration>
    <source>1.8</source>
    <target>1.8</target>
    <compilerArgument>-parameters</compilerArgument>
    </configuration>
    </plugin>
    </plugins>
  2. 自定义序列化器可以自定义SpringConfigProvider的Bean,实现parseValueEncoderparseValueDecoder方法

看源码

Cache的实现:

  • RedisCache: redis实现,使用jedis客户端
  • RedisLettuceCache: redis实现,使用lettuce客户端
  • CaffeineCache: 基于内存的缓存,使用Caffeine
  • LinkedHashMapCache: 自制的简易内存缓存,没有任何依赖
  • LoadingCache:基于Decorator模式,提供自动加载功能
  • RefreshCache:基于Decorator模式,提供自动刷新功能
  • MultiLevelCache:多级缓存,注解方式配置只支持了两级,实际上这个类支持N级

jetcache-anno提供了注解支持,顺着这两个注解类看就可以了:

  • EnableCreateCacheAnnotation
  • EnableMethodCache