引言
redis緩存的有效期可以通過xml配置文件設(shè)置(默認(rèn)有效期),也可以通過編碼的方式手動去設(shè)置,但是這兩種方式都存在缺陷。xml方式設(shè)置的是全局的默認(rèn)有效期,雖然靈活,但不能給某個(gè)緩存設(shè)置單獨(dú)的有效期;硬編碼方式雖然可以給不同的緩存設(shè)置單獨(dú)的有效期,但是管理上不夠靈活。Spring提供的Cache相關(guān)注解中并沒有提供有效期的配置參數(shù),so,自定義注解實(shí)現(xiàn)緩存有效期的靈活設(shè)置誕生了,具體源碼前往github下載。
Redis緩存
如何使用Redis實(shí)現(xiàn)數(shù)據(jù)緩存,請參考上篇《使用Spring-Data-Redis實(shí)現(xiàn)數(shù)據(jù)緩存》。
工具類介紹
1.JedisPoolConfig
jedis連接池配置類,位于jedis包中,用于配置連接池中jedis連接數(shù)的個(gè)數(shù)、是否阻塞、逐出策略等。示例配置如下所示。
<bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig">
<!-- maxIdle最大空閑連接數(shù) -->
<property name="maxIdle" value="${redis.maxIdle}"/>
<!-- maxTotal最大連接數(shù) -->
<property name="maxTotal" value="${redis.maxActive}"/>
<!-- maxWaitMillis獲取連接時(shí)的最大等待毫秒數(shù),小于零表示阻塞不確定的時(shí)間,默認(rèn)為-1 -->
<property name="maxWaitMillis" value="${redis.maxWait}"/>
<!-- testOnBorrow在獲取連接的時(shí)是否檢查有效性 -->
<property name="testOnBorrow" value="${redis.testOnBorrow}"/>
</bean>
2.JedisConnectionFactory
jedis實(shí)例的創(chuàng)建工廠,基于連接池創(chuàng)建jedis實(shí)例,位于spring-data-redis包中。示例配置如下所示。
<bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
<!-- hostName Redis主機(jī)名,默認(rèn)是localhost -->
<property name="hostName" value="${redis.host}"/>
<!-- port Redis提供服務(wù)的端口-->
<property name="port" value="${redis.port}"/>
<!-- password Redis認(rèn)證密碼 -->
<property name="password" value="${redis.pass}"/>
<!-- database 連接工廠使用到的數(shù)據(jù)庫索引,默認(rèn)是0 -->
<property name="database" value="${redis.dbIndex}"/>
<!-- poolConfig 連接池配置 -->
<property name="poolConfig" ref="poolConfig"/>
</bean>
3.RedisTemplate
RedisTemplate可以從JedisConnectionFactory中獲取jedis實(shí)例,封裝了jedis的操作,位于spring-data-redis包中,讓使用者無需關(guān)心連接的獲取及釋放,集中關(guān)注業(yè)務(wù)處理。示例配置如下所示。
<bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
<property name="connectionFactory" ref="jedisConnectionFactory"/>
</bean>
4.RedisCacheManager
使用RedisTemplate對Redis緩存進(jìn)行管理,位于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中一個(gè)重要的方法,void setExpires(Map<String, Long> expires),該方法的傳入?yún)?shù)是一個(gè)Map,Map的key值是@Cacheable(或@CacheEvict或@CachePut)注解的value值,Map的value值是緩存的有效期(單位秒),用于批量設(shè)置緩存的有效期。
自定義注解
直接貼代碼了,如下。
@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
新寫了一個(gè)SpringRedisCacheManager,繼承自RedisCacheManager,用于對@CacheDuration解析及有效期的設(shè)置,代碼如下。
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);
}
//設(shè)置有效期
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標(biāo)注的有效期,優(yōu)先使用方法上標(biāo)注的有效期
* @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實(shí)例 -->
<bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig">
<!-- maxIdle最大空閑連接數(shù) -->
<property name="maxIdle" value="${redis.maxIdle}"/>
<!-- maxTotal最大連接數(shù) -->
<property name="maxTotal" value="${redis.maxActive}"/>
<!-- maxWaitMillis獲取連接時(shí)的最大等待毫秒數(shù),小于零表示阻塞不確定的時(shí)間,默認(rèn)為-1 -->
<property name="maxWaitMillis" value="${redis.maxWait}"/>
<!-- testOnBorrow在獲取連接的時(shí)是否檢查有效性 -->
<property name="testOnBorrow" value="${redis.testOnBorrow}"/>
</bean>
<!-- 配置JedisConnectionFactory -->
<bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
<!-- hostName Redis主機(jī)名,默認(rèn)是localhost -->
<property name="hostName" value="${redis.host}"/>
<!-- port Redis提供服務(wù)的端口-->
<property name="port" value="${redis.port}"/>
<!-- password Redis認(rèn)證密碼 -->
<property name="password" value="${redis.pass}"/>
<!-- database 連接工廠使用到的數(shù)據(jù)庫索引,默認(rèn)是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("第一次執(zhí)行查詢:" + userService.queryFullNameById(100L));
System.out.println("----------------------------------");
System.out.println("第二次執(zhí)行查詢:" + 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("第三次執(zhí)行查詢:" + userService.queryFullNameById(100l));
}
測試結(jié)果
execute queryFullNameById method
第一次執(zhí)行查詢:ZhangSanFeng
----------------------------------
第二次執(zhí)行查詢:ZhangSanFeng
----------------------------------
UserId_100有效期(單位秒):15
----------------------------------
主線程休眠3秒后
----------------------------------
UserId_100有效期(單位秒):12
----------------------------------
第三次執(zhí)行查詢:ZhangSanFeng
結(jié)果分析
UserService類上標(biāo)注的CacheDuration設(shè)置有效期是6秒,而方法queryFullNameById上CacheDuration設(shè)置的有效期是16秒,最后生效的是16秒。