基于注释(annotation)的缓存(cache)技术是在Spring 3.1 引入的,它本质上不是一个具体的缓存实现方案(例如 EHCache),而是一个对缓存使用的抽象,通过在既有代码中添加少量它定义的各种 annotation,就能够达到缓存方法的返回对象的效果。
Spring 的缓存技术还具备相当的灵活性,不仅能够使用 SpEL(Spring Expression Language)来定义缓存的 key 和各种 condition,还提供开箱即用的缓存临时存储方案,也支持和主流的专业缓存例如Redis集成。
主要特点如下:
spring cache 常用注解: @Cacheable、@CachePut 和 @CacheEvict
2.1、@Cacheable
作用: 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存。
参数介绍:
value:缓存的名称
每一个缓存名称代表一个缓存对象。当一个方法填写多个缓存名称时将创建多个缓存对象。当多个方法使用同一缓存名称时相同参数的缓存会被覆盖。所以通常情况我们使用“包名+类名+方法名”或者使用接口的RequestMapping作为缓存名称防止命名重复引起的问题。
单缓存名称:@Cacheable(value=”mycache”)
多缓存名称:@Cacheable(value={”cache1”,”cache2”}
key:缓存的 key
key标记了缓存对象下的每一条缓存。如果不指定key则系统自动按照方法的所有入参生成key,也就是说相同的入参值将会返回同样的缓存结果。
如果指定key则要按照 SpEL 表达式编写使用的入参列表。如下列无论方法存在多少个入参,只要userName值一致,则会返回相同的缓存结果。
@Cacheable(value=”testcache”,key=”#userName”)
condition:缓存的条件
满足条件后方法结果才会被缓存。不填写则认为无条件全部缓存。
条件使用 SpEL表达式编写,返回 true 或者 false,只有为 true 才进行缓存
如下例,只有用户名长度大于2时参会进行缓存
@Cacheable(value=”testcache”,condition=”#userName.length()>2”)
2.2、@CachePut
作用: 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存。和 @Cacheable 不同的是,它每次都会触发真实方法的调用,此注解常被用于更新缓存使用。
参数介绍:
value:缓存的名称
@CachePut(value=”mycache”)
@CachePut(value={”cache1”,”cache2”}
key:缓存的 key
@CachePut(value=”testcache”,key=”#userName”)
condition:缓存的条件
@CachePut(value=”testcache”,condition=”#userName.length()>2”)
2.3、@CacheEvict
作用: 主要针对方法配置,能够根据一定的条件对缓存进行清空
参数介绍:
value 缓存的名称
删除指定名称的缓存对象。必须与下面的其中一个参数配合使用
例如:
@CacheEvict(value=”mycache”) 或者
@CacheEvict(value={”cache1”,”cache2”}
key 缓存的 key
删除指定key的缓存对象
例如:
@CacheEvict(value=”testcache”,key=”#userName”)
condition 缓存的条件
删除指定条件的缓存对象
例如:
@CacheEvict(value=”testcache”,condition=”#userName.length()>2”)
allEntries 方法执行后清空所有缓存
缺省为 false,如果指定为 true,则方法调用后将立即清空所有缓存。
例如:
@CacheEvict(value=”testcache”,allEntries=true)
beforeInvocation 方法执行前清空所有缓存
缺省为 false,如果指定为 true,则在方法还没有执行的时候就清空缓存,缺省情况下,如果方法执行抛出异常,则不会清空缓存。
例如:
@CacheEvict(value=”testcache”,beforeInvocation=true)
Spring Boot 为我们提供了多种缓存CacheManager配置方案。默认情况下会使用基于内存map一种缓存方案ConcurrenMapCacheManager。当然我没也可以通过配置使用 Generic、JCache (JSR-107)、EhCache 2.x、Hazelcast、Infinispan、Redis、Guava、Simple等技术进行缓存实现。
这里使用默认的基于内存的方案进行举例
引入依赖
在pom文件中引入缓存包
org.springframework.boot
spring-boot-starter-cache
启用缓存
在启动类增加启用缓存注解@EnableCaching,该注解主要是用于spring framework中的注解驱动的缓存管理。如果使用了该注解,则就不需要在XML文件中配置cache manager了。
@SpringBootApplication
@EnableCaching //启用缓存
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}
缓存测试方法
测试方法做了一个2秒的延时
public class CacheTest {
/**
* 缓存测试方法延时两秒
* @param i
* @return
*/
@Cacheable(value = "cache_test")
public String cacheFunction(int i){
try {
long time = 2000L;
Thread.sleep(time);
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
return "success"+ i;
}
}
调用缓存测试方法
这里需要注意:不能在同一个类中调用被注解缓存了的方法。也就是说缓存调用方法和缓存注解方法不能在一个类中出现。
public class HelloController {
@Autowired
CacheTest cacheTest;
@GetMapping(value = "/")
public String hello(){
for(int i=0;i<5;i++){
System.out.println(new Date() + " " + cacheTest.cacheFunction(i));
}
return "/hello";
}
}
测试结果
我们可以看出第一次执行时每间隔2秒打印了一次success
而第二次同一时间全部打印完成
Tue Jun 12 15:35:01 CST 2018 success0
Tue Jun 12 15:35:03 CST 2018 success1
Tue Jun 12 15:35:05 CST 2018 success2
Tue Jun 12 15:35:07 CST 2018 success3
Tue Jun 12 15:35:09 CST 2018 success4
Tue Jun 12 15:35:26 CST 2018 success0
Tue Jun 12 15:35:26 CST 2018 success1
Tue Jun 12 15:35:26 CST 2018 success2
Tue Jun 12 15:35:26 CST 2018 success3
Tue Jun 12 15:35:26 CST 2018 success4
Spring Cache集成redis的运行原理
Spring缓存抽象模块通过CacheManager来创建、管理实际缓存组件,当SpringBoot应用程序引入spring-boot-starter-data-redis依赖后,容器中将注册的是CacheManager实例RedisCacheManager对象,RedisCacheManager来负责创建RedisCache作为缓存管理组件,由RedisCache操作redis服务器实现缓存数据操作。
引入redis依赖
在pom文件中引入redis
org.springframework.boot
spring-boot-starter-data-redis
redis配置如下
在application.properties配置文件中增加redis配置
#redis配置
#Redis数据库索引(缓存将使用此索引编号的数据库)
spring.redis.database=10
#Redis服务器地址
spring.redis.host=127.0.0.1
#Redis服务器连接端口
spring.redis.port=6379
#Redis服务器连接密码(默认为空)
spring.redis.password=******
#连接超时时间 毫秒(默认2000)
#请求redis服务的超时时间,这里注意设置成0时取默认时间2000
spring.redis.timeout=2000
#连接池最大连接数(使用负值表示没有限制)
#建议为业务期望QPS/一个连接的QPS,例如50000/1000=50
#一次命令时间(borrow|return resource+Jedis执行命令+网络延迟)的平均耗时约为1ms,一个连接的QPS大约是1000
spring.redis.pool.max-active=50
#连接池中的最大空闲连接
#建议和最大连接数一致,这样做的好处是连接数从不减少,从而避免了连接池伸缩产生的性能开销。
spring.redis.pool.max-idle=50
#连接池中的最小空闲连接
#建议为0,在无请求的状况下从不创建链接
spring.redis.pool.min-idle=0
#连接池最大阻塞等待时间 毫秒(-1表示没有限制)
#建议不要为-1,连接池占满后无法获取连接时将在该时间内阻塞等待,超时后将抛出异常。
spring.redis.pool.max-wait=2000
设置缓存生存时间
我们可以对redis缓存数据指定生存时间从而达到缓存自动失效的目的。
通过创建缓存配置文件类可以设置缓存各项参数
@Configuration
public class RedisCacheConfig {
@Bean
public RedisCacheManager cacheManager(RedisTemplate redisTemplate) {
//获得redis缓存管理类
RedisCacheManager redisCacheManager = new RedisCacheManager(redisTemplate);
// 开启使用缓存名称做为key前缀(这样所有同名缓存会整理在一起比较容易查找)
redisCacheManager.setUsePrefix(true);
//这里可以设置一个默认的过期时间 单位是秒
redisCacheManager.setDefaultExpiration(600L);
// 设置缓存的过期时间 单位是秒
Map expires = new HashMap<>();
expires.put("pub.imlichao.CacheTest.cacheFunction", 100L);
redisCacheManager.setExpires(expires);
return redisCacheManager;
}
}
设置过期时间时也可以不采用expires.put(“pub.imlichao.CacheTest.cacheFunction”, 100L)的写法,而是使用@Cacheable标签的value值进行声明,如下
@Configuration
public class RedisCacheConfig {
......
// 设置缓存的过期时间 单位是秒
Map expires = new HashMap<>();
expires.put("cache_test", 100L);
redisCacheManager.setExpires(expires);
return redisCacheManager;
}
}
设置缓存序列化方式
redisTemplate 默认的序列化方式为 jdkSerializeable,我们也可以使用其他序列化方式来达到不同的需求。比如我们希望缓存的数据具有可读性就可以将其序列化为json格式,json序列化可以使用Jackson2JsonRedisSerialize或FastJsonRedisSerializer。如果我们希望拥有更快的速度和占用更小的存储空间推荐使用KryoRedisSerializer进行序列化。
由于redis缓存对可读性没什么要求,而存储空间和速度是比较重要的,所以这里使用KryoRedisSerializer进行对象序列化。
添加Kryo依赖
com.esotericsoftware
kryo
4.0.2
实现RedisSerializer接口创建KryoRedisSerializer序列化工具
import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
import java.io.ByteArrayOutputStream;
public class KryoRedisSerializer implements RedisSerializer {
public static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
private static final ThreadLocal kryos = ThreadLocal.withInitial(Kryo::new);
private Class clazz;
public KryoRedisSerializer(Class clazz) {
super();
this.clazz = clazz;
}
@Override
public byte[] serialize(T t) throws SerializationException {
if (t == null) {
return EMPTY_BYTE_ARRAY;
}
Kryo kryo = kryos.get();
kryo.setReferences(false);
kryo.register(clazz);
try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
Output output = new Output(baos)) {
kryo.writeClassAndObject(output, t);
output.flush();
return baos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
}
return EMPTY_BYTE_ARRAY;
}
@Override
public T deserialize(byte[] bytes) throws SerializationException {
if (bytes == null || bytes.length <= 0) {
return null;
}
Kryo kryo = kryos.get();
kryo.setReferences(false);
kryo.register(clazz);
try (Input input = new Input(bytes)) {
return (T) kryo.readClassAndObject(input);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
修改配置文件替换默认序列化工具
@Configuration
public class RedisCacheConfig {
@Bean("redisTemplate")
public RedisTemplate