引言
redis缓存的有效期可以通过xml配置文件设置(默认有效期),也可以通过编码的方式手动去设置,但是这两种方式都存在缺陷。xml方式设置的是全局的默认有效期,虽然灵活,但不能给某个缓存设置单独的有效期;硬编码方式虽然可以给不同的缓存设置单独的有效期,但是管理上不够灵活。Spring提供的Cache相关注解中并没有提供有效期的配置参数,so,自定义注解实现缓存有效期的灵活设置诞生了,具体源码前往github下载。
Redis缓存
如何使用Redis实现数据缓存,请参考上篇《使用Spring-Data-Redis实现数据缓存》。
工具类介绍
1.JedisPoolConfig
jedis连接池配置类,位于jedis包中,用于配置连接池中jedis连接数的个数、是否阻塞、逐出策略等。示例配置如下所示。
<bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig">
<!-- maxIdle最大空闲连接数 -->
<property name="maxIdle" value="${redis.maxIdle}"/>
<!-- maxTotal最大连接数 -->
<property name="maxTotal" value="${redis.maxActive}"/>
<!-- maxWaitMillis获取连接时的最大等待毫秒数,小于零表示阻塞不确定的时间,默认为-1 -->
<property name="maxWaitMillis" value="${redis.maxWait}"/>
<!-- testOnBorrow在获取连接的时是否检查有效性 -->
<property name="testOnBorrow" value="${redis.testOnBorrow}"/>
</bean>
2.JedisConnectionFactory
jedis实例的创建工厂,基于连接池创建jedis实例,位于spring-data-redis包中。示例配置如下所示。
<bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
<!-- hostName Redis主机名,默认是localhost -->
<property name="hostName" value="${redis.host}"/>
<!-- port Redis提供服务的端口-->
<property name="port" value="${redis.port}"/>
<!-- password Redis认证密码 -->
<property name="password" value="${redis.pass}"/>
<!-- database 连接工厂使用到的数据库索引,默认是0 -->
<property name="database" value="${redis.dbIndex}"/>
<!-- poolConfig 连接池配置 -->
<property name="poolConfig" ref="poolConfig"/>
</bean>
3.RedisTemplate
RedisTemplate可以从JedisConnectionFactory中获取jedis实例,封装了jedis的操作,位于spring-data-redis包中,让使用者无需关心连接的获取及释放,集中关注业务处理。示例配置如下所示。
<bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
<property name="connectionFactory" ref="jedisConnectionFactory"/>
</bean>
4.RedisCacheManager
使用RedisTemplate对Redis缓存进行管理,位于spring-data-redis包中。示例配置如下所示。
<bean id="redisCacheManager" class="org.springframework.data.redis.cache.RedisCacheManager">
<constructor-arg name="redisOperations" ref="redisTemplate"/>
<property name="defaultExpiration" value="${redis.expiration}"/>
</bean>
这里介绍RedisCacheManager中一个重要的方法,void setExpires(Map<String, Long> expires),该方法的传入参数是一个Map,Map的key值是@Cacheable(或@CacheEvict或@CachePut)注解的value值,Map的value值是缓存的有效期(单位秒),用于批量设置缓存的有效期。
自定义注解
直接贴代码了,如下。
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface CacheDuration {
//Sets the expire time (in seconds).
public long duration() default 60;
}
使用@CacheDuration
@Service("userService")
@CacheDuration(duration = 6)
public class UserService {
@Cacheable(value = "User", key = "'UserId_' + #id", condition = "#id<=110")
@CacheDuration(duration = 16)
public String queryFullNameById(long id) {
System.out.println("execute queryFullNameById method");
return "ZhangSanFeng";
}
}
新RedisCacheManager
新写了一个SpringRedisCacheManager,继承自RedisCacheManager,用于对@CacheDuration解析及有效期的设置,代码如下。
public class SpringRedisCacheManager extends RedisCacheManager implements ApplicationContextAware, InitializingBean {
private ApplicationContext applicationContext;
public SpringRedisCacheManager(RedisOperations redisOperations) {
super(redisOperations);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
@Override
public void afterPropertiesSet() {
parseCacheDuration(applicationContext);
}
private void parseCacheDuration(ApplicationContext applicationContext) {
final Map<String, Long> cacheExpires = new HashMap<>();
String[] beanNames = applicationContext.getBeanNamesForType(Object.class);
for (String beanName : beanNames) {
final Class clazz = applicationContext.getType(beanName);
Service service = findAnnotation(clazz, Service.class);
if (null == service) {
continue;
}
addCacheExpires(clazz, cacheExpires);
}
//设置有效期
super.setExpires(cacheExpires);
}
private void addCacheExpires(final Class clazz, final Map<String, Long> cacheExpires) {
ReflectionUtils.doWithMethods(clazz, new ReflectionUtils.MethodCallback() {
@Override
public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException {
ReflectionUtils.makeAccessible(method);
CacheDuration cacheDuration = findCacheDuration(clazz, method);
Cacheable cacheable = findAnnotation(method, Cacheable.class);
CacheConfig cacheConfig = findAnnotation(clazz, CacheConfig.class);
Set<String> cacheNames = findCacheNames(cacheConfig, cacheable);
for (String cacheName : cacheNames) {
cacheExpires.put(cacheName, cacheDuration.duration());
}
}
}, new ReflectionUtils.MethodFilter() {
@Override
public boolean matches(Method method) {
return null != findAnnotation(method, Cacheable.class);
}
});
}
/**
* CacheDuration标注的有效期,优先使用方法上标注的有效期
* @param clazz
* @param method
* @return
*/
private CacheDuration findCacheDuration(Class clazz, Method method) {
CacheDuration methodCacheDuration = findAnnotation(method, CacheDuration.class);
if (null != methodCacheDuration) {
return methodCacheDuration;
}
CacheDuration classCacheDuration = findAnnotation(clazz, CacheDuration.class);
if (null != classCacheDuration) {
return classCacheDuration;
}
throw new IllegalStateException("No CacheDuration config on Class " + clazz.getName() + " and method " + method.toString());
}
private Set<String> findCacheNames(CacheConfig cacheConfig, Cacheable cacheable) {
return isEmpty(cacheable.value()) ?
newHashSet(cacheConfig.cacheNames()) : newHashSet(cacheable.value());
}
}
Spring的xml配置
完整配置redisCacheContext.xml如下所示。
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.springframework.org/schema/beans"
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.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd">
<context:component-scan base-package="redis.cache"/>
<context:annotation-config/>
<cache:annotation-driven cache-manager="redisCacheManager"/>
<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="locations">
<list>
<value>classpath:redis.properties</value>
</list>
</property>
</bean>
<!-- 配置JedisPoolConfig实例 -->
<bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig">
<!-- maxIdle最大空闲连接数 -->
<property name="maxIdle" value="${redis.maxIdle}"/>
<!-- maxTotal最大连接数 -->
<property name="maxTotal" value="${redis.maxActive}"/>
<!-- maxWaitMillis获取连接时的最大等待毫秒数,小于零表示阻塞不确定的时间,默认为-1 -->
<property name="maxWaitMillis" value="${redis.maxWait}"/>
<!-- testOnBorrow在获取连接的时是否检查有效性 -->
<property name="testOnBorrow" value="${redis.testOnBorrow}"/>
</bean>
<!-- 配置JedisConnectionFactory -->
<bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
<!-- hostName Redis主机名,默认是localhost -->
<property name="hostName" value="${redis.host}"/>
<!-- port Redis提供服务的端口-->
<property name="port" value="${redis.port}"/>
<!-- password Redis认证密码 -->
<property name="password" value="${redis.pass}"/>
<!-- database 连接工厂使用到的数据库索引,默认是0 -->
<property name="database" value="${redis.dbIndex}"/>
<!-- poolConfig 连接池配置 -->
<property name="poolConfig" ref="poolConfig"/>
</bean>
<!-- 配置RedisTemplate -->
<bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
<property name="connectionFactory" ref="jedisConnectionFactory"/>
</bean>
<!-- 配置RedisCacheManager -->
<bean id="redisCacheManager" class="redis.cache.SpringRedisCacheManager">
<constructor-arg name="redisOperations" ref="redisTemplate"/>
<property name="defaultExpiration" value="${redis.expiration}"/>
</bean>
</beans>
Redis连接配置
完整配置如下。
redis.host=127.0.0.1
redis.port=6379
redis.pass=
redis.dbIndex=0
redis.expiration=3000
redis.maxIdle=300
redis.maxActive=600
redis.maxWait=1000
redis.testOnBorrow=true
测试代码
@Test
public void testRedisCacheManager() {
ApplicationContext context = new ClassPathXmlApplicationContext("redisCacheContext.xml");
UserService userService = (UserService) context.getBean("userService");
RedisTemplate redisTemplate = (RedisTemplate) context.getBean("redisTemplate");
System.out.println("第一次执行查询:" + userService.queryFullNameById(100L));
System.out.println("----------------------------------");
System.out.println("第二次执行查询:" + userService.queryFullNameById(100L));
System.out.println("----------------------------------");
System.out.println("UserId_100有效期(单位秒):" + redisTemplate.getExpire("UserId_100", TimeUnit.SECONDS));
System.out.println("----------------------------------");
try {
Thread.sleep(3000);
System.out.println("主线程休眠3秒后");
System.out.println("----------------------------------");
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("UserId_100有效期(单位秒):" + redisTemplate.getExpire("UserId_100", TimeUnit.SECONDS));
System.out.println("----------------------------------");
System.out.println("第三次执行查询:" + userService.queryFullNameById(100l));
}
测试结果
execute queryFullNameById method
第一次执行查询:ZhangSanFeng
----------------------------------
第二次执行查询:ZhangSanFeng
----------------------------------
UserId_100有效期(单位秒):15
----------------------------------
主线程休眠3秒后
----------------------------------
UserId_100有效期(单位秒):12
----------------------------------
第三次执行查询:ZhangSanFeng
结果分析
UserService类上标注的CacheDuration设置有效期是6秒,而方法queryFullNameById上CacheDuration设置的有效期是16秒,最后生效的是16秒。