Content Table

Spring MVC 中使用 JetCache

JetCache 是一个基于 Java 的缓存系统封装,提供统一的API和注解来简化缓存的使用。 JetCache 提供了比 SpringCache 更加强大的注解,可以原生的支持 TTL、两级缓存、分布式自动刷新,还提供了 Cache 接口用于手工缓存操作。 当前有四个实现,RedisCacheTairCache (此部分未在 github 开源)、CaffeineCache (in memory) 和一个简易的LinkedHashMapCache (in memory),要添加新的实现也是非常简单的。

网上很多文章介绍 JetCache 的文章包括官方文档主要是基于 Spring Boot 的,也介绍了未使用 SpringBoot 的配置方式,但是估计很多同学还是不明白怎么在传统的 Spring MVC 的 Web 项目里使用 JetCache 吧,毕竟不是所有 Web 项目都使用 Spring Boot,接下来就一步一步的介绍使用的方法。

提示:

  • JetCache 需要 Java 8,并且指定 javac 的 -parameters 参数
  • JetCache 有 3 个重要的注解: @Cached, @CacheUpdate, @CacheInvalidate,详细文档请参考 MethodCache_CN
  • JetCache 不支持 Redis 的 Hash, List, Set 等,如果需要的话,使用 client 进行访问吧

一、Gradle 依赖

1
2
3
4
dependencies {
compile group: 'com.alicp.jetcache', name: 'jetcache-anno', version: '2.5.13'
compile group: 'com.alicp.jetcache', name: 'jetcache-redis', version: '2.5.13'
}

二、JVM 设置

JetCache 需要 Java 8,并且指定 javac 的 -parameters 参数,在 build.gradle 中设置 JVM 相关信息如下:

1
2
3
4
5
6
7
8
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
[compileJava, compileTestJava, javadoc]*.options*.encoding = 'UTF-8'

compileJava {
options.compilerArgs << '-Xlint:unchecked' << '-Xlint:deprecation' << '-parameters'
options.forkOptions.jvmArgs << '-parameters'
}

三、JetCache 配置

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
package com.xtuer.config;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import com.alicp.jetcache.CacheBuilder;
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;

/**
* JetCache 的配置
*/
@Configuration
@EnableMethodCache(basePackages = "com.xtuer.service")
@EnableCreateCacheAnnotation
public class JetCacheConfig {
@Bean
public Pool<Jedis> 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<Jedis> pool){
Map<String, CacheBuilder> localBuilders = new HashMap<>();
EmbeddedCacheBuilder localBuilder = LinkedHashMapCacheBuilder
.createLinkedHashMapCacheBuilder()
.keyConvertor(FastjsonKeyConvertor.INSTANCE);
localBuilders.put(CacheConsts.DEFAULT_AREA, localBuilder);

Map<String, CacheBuilder> remoteBuilders = new HashMap<>();
RedisCacheBuilder remoteCacheBuilder = RedisCacheBuilder.createRedisCacheBuilder()
.keyConvertor(FastjsonKeyConvertor.INSTANCE)
.valueEncoder(JavaValueEncoder.INSTANCE) // 可替换为 KryoValueEncoder.INSTANCE
.valueDecoder(JavaValueDecoder.INSTANCE) // 可替换为 KryoValueDecoder.INSTANCE
.expireAfterWrite(3600, TimeUnit.SECONDS) // 全局 expire,@Cached 能够指定自己的 expire
.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;
}
}

提示: 使用 JavaValueEncoder 进行缓存的 Bean 需要实现 Serializable 接口,使用 KryoValueEncoder 进行缓存的 Bean 不需要实现 Serializable 接口,并且性能更好。

四、加载 JetCache 配置

在 Spring MVC 项目的 xml 配置文件中使用自动扫描包创建 JetCacheConfig 的对象或者手动创建,下面使用自动扫描的方式:

1
<context:component-scan base-package="com.xtuer.config"/>

五、Service 和 Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.xtuer.service;

import com.alicp.jetcache.anno.CacheInvalidate;
import com.alicp.jetcache.anno.Cached;
import org.springframework.stereotype.Service;

@Service
public class HelloService {
@Cached(name = "user.", key = "#userId", expire = 600)
public String getUsernameById(long userId) {
System.out.println("Fetch username from DB");
return "Bob";
}

@CacheInvalidate(name = "user.", key = "#userId")
public void removeUsername(long userId) {
System.out.println("Remove user from Redis");
}
}
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
package com.xtuer.controller;

import com.xtuer.service.HelloService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class HelloController {
@Autowired
private HelloService helloService;

/**
* 网址: http://localhost:8080/api/cache
*/
@GetMapping("/api/cache")
@ResponseBody
public String cache() {
return helloService.getUsernameById(10);
}

/**
* 网址: http://localhost:8080/api/cache-invalidate
*/
@GetMapping("/api/cache-invalidate")
@ResponseBody
public String cacheInvalidate() {
helloService.removeUsername(10);

return "OK";
}
}

六、缓存测试

上面就是所有需要的代码和设置,然后启动项目进行测试:

  1. 启动 Redis
  2. 启动 Web 项目
  3. 浏览器中输入 http://localhost:8080/api/cache,可以看到控制台输出 Get from db,Redis 中创建了缓存
  4. 再次刷新缓存,控制台不再输出,直接从 Redis 中读取
  5. 浏览器输入 http://localhost:8080/api/cache-invalidate,发现 Redis 中的缓存被删除了

七、补充说明

以下几点需要注意一下:

  • @Cached

    • cacheType: 默认为 CacheType.REMOTE,使用 Redis 作为缓存
    • name: 为缓存对象的名字,一个 name 对应一个 cache 对象,使用 Redis 缓存时 name 作为 Redis Key 的前缀,同一个 area 中不同作用的 @Cached 的 name 最好设置为不同的值,例如用户的缓存,试卷的缓存使用不同的名字,这样有利于查看不同缓存的统计信息,参考最后的缓存信息统计
    • key: 使用 SpEL 表达式,可以使用字符串连接构建复杂的 key,例如 @Cached(name = "user.", key = "#userId + '.name'", expire = 600) 生成的 key 如 user.10000.name,user.20002.name,如果不指定 name,则自动生成一个,如 c.x.s.HelloService.getUsernameById(J),这样在 @CacheInvalidate 的时候就不知道缓存对象的名字而无法删除缓存了,所以如果需要删除缓存时一定要手动指定缓存的名字
  • null: 默认 null 值是不会被缓存

  • 被缓存的对象需要实现 Serializable 接口

  • 在同一个类中调用本类的另一个方法没有触发 AOP 的问题,也即 @Cached 注解不生效,这是常见的 Spring AOP 嵌套调用时内部 AOP 失效的问题,例如

    service.funcB() 使用 @Cached 标记,service.funcA() 中调用 this.funcB(),此时 funcB() 的 @Cached 不会生效,因为调用 funcA() 的对象 service 是 AOP 生成的代理对象,funcA() 内的 this 是被代理对象而不是代理对象,此时的 funcB() 是我们定义类时写的方法,不是 AOP 后生成的方法。
    解决的办法也可以很简单,在 bean 中通过 Autowired 注解注入自己 self,this.funcB() 换为 self.funcB(),可以参考这篇文章这篇文章

  • 禁用 JetCache 缓存: 只需要把类 JetCacheConfig 上的注解 @Configuration 去掉即可,在测试开发阶段需要不停的从数据库读取数据进行校验时就方便多了,不需要总是去删除缓存数据

缓存信息统计:

1
2
3
4
5
[StatInfoLogger.java-accept:46] - jetcache stat from 2019-07-18 06:29:00,014 to 2019-07-18 06:30:00,003
cache| qps| rate| get| hit| fail| expire|avgLoadTime|maxLoadTime
-----+----------+-------+--------------+--------------+--------------+--------------+-----------+-----------
user.| 0.07| 75.00%| 4| 3| 0| 0| 1.0| 1
-----+----------+-------+--------------+--------------+--------------+--------------+-----------+-----------