SpringCache整合Redis实现自定义缓存时间
Spring Cache简介
Spring3.1开始引入了的基于注释(annotation)的缓存(cache)技术,它本质上不是一个具体的缓存实现方案,而是一个对缓存使用的抽象,通过在既有代码中添加注解,即能够达到缓存方法的返回对象的效果。
Spring 的缓存技术还具备相当的灵活性,不仅能够使用 SpEL 来定义缓存的 key 和各种 condition,还提供开箱即用的缓存临时存储方案,也支持和主流的专业缓存例如 Redis 集成。
@Cacheable
这个用的比较多,用在查询方法上,先从缓存中读取,如果缓存不存在再调用该方法获取数据,然后把返回的数据添加到缓存中去。
@Cacheable(value = "userCache", key = "targetClass + '.' + methodName + '.' + "#userid")
public User getEntity(long userid) {
// 业务代码省略
}
@CacheEvict
清空缓存
@CacheEvict(value = "userCache", key = "targetClass + '.' + methodName + '.' + "#userid")
public boolean delete(long userid) {
// 业务代码省略
}
@CachePut
这个注释可以确保方法被执行,同时方法的返回值也被记录到缓存中,实现缓存与数据库的同步更新。
@CachePut(value = "userCache", key = "targetClass + '.' + methodName + '.' + "#user.getUserid")
public User save(User user) {
// 业务代码省略
}
注:每个注解都有多个参数,这里不一一列出,建议进入源码查看注释。
缺点
虽然Spring Cache用起来很方便的, 但不支持设置动态过期时间,这里需要重写RedisCacheManager的一些方法。
示例
这里用的spring对redis的封装spring-data-redis,主要是对RedisCacheManager做一个二次封装。
导包
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>1.8.4.RELEASE</version>
</dependency>
重写 RedisCacheManager
package com.demo.cache;
import java.util.Objects;
import java.util.regex.Pattern;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import org.apache.commons.lang3.math.NumberUtils;
import org.springframework.data.redis.cache.RedisCache;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.core.RedisOperations;
import lombok.extern.log4j.Log4j2;
/**
* 重写redis缓存管理器
* <p>
* 重写 RedisCacheManager createCache 方法
* <p>
* 在缓存名字上添加过期时间表达式 如:cachename#60*60
* @author czk
*/
@Log4j2
public class ExtendedRedisCacheManager extends RedisCacheManager {
private static final ScriptEngine scriptEngine = new ScriptEngineManager().getEngineByName("JavaScript");
private static final Pattern pattern = Pattern.compile("[+\\-*/%]");
/**
* 分隔符
*/
private char separator = '#';
public ExtendedRedisCacheManager(@SuppressWarnings("rawtypes") RedisOperations redisOperations) {
super(redisOperations);
}
@Override
@SuppressWarnings("unchecked")
protected RedisCache createCache(String cacheName) {
// 获取默认时间
long expiration = computeExpiration(cacheName);
int index = cacheName.indexOf(this.getSeparator());
if (index > 0) {
expiration = getExpiration(cacheName, index, expiration);
}
return new RedisCache(cacheName, (isUsePrefix() ? getCachePrefix().prefix(cacheName) : null),
getRedisOperations(), expiration);
}
/**
* 计算缓存时间
* @param name 缓存名字 cache#60*60
* @param separatorIndex 分隔符位置
* @param defalutExp 默认缓存时间
* @return
*/
protected long getExpiration(final String name, final int separatorIndex, final long defalutExp) {
Long expiration = null;
String expirationAsString = name.substring(separatorIndex + 1);
try {
if (pattern.matcher(expirationAsString).find()) {
expiration = NumberUtils.toLong(scriptEngine.eval(expirationAsString).toString(), defalutExp);
} else {
expiration = NumberUtils.toLong(expirationAsString, defalutExp);
}
} catch (ScriptException e) {
log.error("缓存时间转换错误:{},异常:{}", name, e.getMessage());
}
return Objects.nonNull(expiration) ? expiration.longValue() : defalutExp;
}
public char getSeparator() {
return separator;
}
public void setSeparator(char separator) {
this.separator = separator;
}
}
spring-redis.xml配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:cache="http://www.springframework.org/schema/cache"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.3.xsd
http://www.springframework.org/schema/cache
http://www.springframework.org/schema/cache/spring-cache-4.3.xsd">
<context:property-placeholder location="classpath:redis.properties" />
<!-- 启用缓存注解功能,否则注解不会生效 -->
<cache:annotation-driven cache-manager="cacheManager" />
<!-- redis 相关配置 -->
<bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig">
<property name="maxIdle" value="${redis.maxIdle}" />
<property name="maxWaitMillis" value="${redis.maxWait}" />
<property name="testOnBorrow" value="${redis.testOnBorrow}" />
</bean>
<bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory"
p:host-name="${redis.host}" p:port="${redis.port}" p:password="${redis.password}"
p:database="${redis.database}" p:timeout="${redis.timeout}"
p:pool-config-ref="poolConfig" />
<bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
<property name="connectionFactory" ref="jedisConnectionFactory" />
<!--对key的序列化器 -->
<property name="keySerializer">
<bean class="org.springframework.data.redis.serializer.StringRedisSerializer" />
</property>
<!--是对value的列化器 默认:JdkSerializationRedisSerializer -->
<property name="valueSerializer">
<bean class="org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer" />
</property>
</bean>
<!-- 扩展RedisCacheManager -->
<bean id="cacheManager" class="com.demo.cache.ExtendedRedisCacheManager">
<constructor-arg ref="redisTemplate" />
<!-- 是否使用前缀 默认: -->
<property name="usePrefix" value="true" />
<!-- 默认有效期1h (60 * 60 = 3600秒) -->
<property name="defaultExpiration" value="3600" />
</bean>
</beans>
redis.properties
#redis 缓存配置
redis.host=127.0.0.1
redis.port=6379
redis.password=
redis.database=0
# 控制一个pool最多有多少个状态为idle(空闲的)的jedis实例
redis.maxIdle=300
redis.maxctive=6000
# 表示当borrow(引入)一个jedis实例时,最大的等待时间,如果超过等待时间(毫秒),则直接抛出JedisConnectionException;
redis.maxWait=10000
#在borrow一个jedis实例时,是否提前进行validate操作;如果为true,则得到的jedis实例均是可用的
redis.testOnBorrow=true
#读超时时间
redis.timeout=30000
注: Spring Cache是采用AOP来管理缓存,所有通过this调用的方法多不会触发缓存,key采用的是StringRedisSerializer序列化,所有key必须为String类型。
@Cacheable指定缓存5分钟
@Cacheable(value = "userCache#60*5", key = "targetClass + '.' + methodName + '.' + "#userid")
public User getEntity(long userid) {
// 业务代码省略
}
所有的进步都是不稳定, 一个问题解决了又不得不面对一个新的问题。