扩展基于注解的spring缓存,使缓存有效期的设置支持方法级别-redis篇

原创
2016/03/21 10:23
阅读数 1.2W

这里用的spring对redis的封装spring-data-redis,主要是对RedisCacheManager做一个二次封装。

主要依赖包:

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.7.3</version>
</dependency>
<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-redis</artifactId>
    <version>1.6.0.RELEASE</version>
</dependency>

以下为扩展类com.caiya.cache.redis.ExtendedRedisCacheManager:

package com.caiya.cache.redis;

import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;
import org.springframework.cache.Cache;
import org.springframework.data.redis.cache.RedisCache;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCachePrefix;
import org.springframework.data.redis.core.RedisOperations;

import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import java.util.Collection;
import java.util.Collections;
import java.util.regex.Pattern;

/**
 * CacheManager backed by a Simple Spring Redis (SSR) {@link RedisCache}. Spring Cache and
 * CacheManager doesn't support configuring expiration time per method (there is no dedicated parameter in cache
 * annotation to pass expiration time). This extension of {@link RedisCacheManager} overcomes this limitation and allow to
 * pass expiration time as a part of cache name. To define custom expiration on method as a cache name use concatenation
 * of specific cache name, separator and expiration e.g.
 * <p/>
 * <pre>
 * public class UserDAO {
 *
 *     // cache name: userCache, expiration: 300s
 *     &#064;Cacheable(&quot;userCache#300&quot;)// or &#064;Cacheable(&quot;#60 * 5&quot;)
 *     public User getUser(String name) {
 *
 *     }
 * }
 * </pre>
 *
 * @author caiya
 * @since 16/3/18
 */
public class ExtendedRedisCacheManager extends RedisCacheManager {

    private static final Logger logger = Logger.getLogger(ExtendedRedisCacheManager.class);

    private static final ScriptEngine scriptEngine = new ScriptEngineManager().getEngineByName("JavaScript");

    private static final Pattern pattern = Pattern.compile("[+\\-*/%]");

    private String defaultCacheName;

    private char separator = '#';

    public ExtendedRedisCacheManager(RedisOperations redisOperations) {
        this(redisOperations, Collections.<String>emptyList());
    }

    public ExtendedRedisCacheManager(RedisOperations redisOperations, Collection<String> cacheNames) {
        super(redisOperations, cacheNames);
    }

    @Override
    public Cache getCache(String name) {
        // try to get cache by name
        RedisCache cache = (RedisCache) super.getCache(name);
        if (cache != null) {
            return cache;
        }

        // there's no cache which has given name
        // find separator in cache name
        int index = name.lastIndexOf(getSeparator());
        if (index < 0) {
            return null;
        }

        // split name by the separator
        String cacheName = name.substring(0, index);
        if(StringUtils.isBlank(cacheName)){
            cacheName = defaultCacheName;
        }
        cache = (RedisCache) super.getCache(cacheName);
        if (cache == null) {
            return null;
        }

        // get expiration from name
        Long expiration = getExpiration(name, index);
        if (expiration == null || expiration < 0) {
            logger.warn("Default expiration time will be used for cache '{}' because cannot parse '{}', cacheName : " + cacheName + ", name : " + name);
            return cache;
        }

        return new RedisCache(cacheName, (isUsePrefix() ? getCachePrefix().prefix(cacheName) : null), getRedisOperations(), expiration);
    }


    public char getSeparator() {
        return separator;
    }

    /**
     * Char that separates cache name and expiration time, default: #.
     *
     * @param separator
     */
    public void setSeparator(char separator) {
        this.separator = separator;
    }

    private Long getExpiration(final String name, final int separatorIndex) {
        Long expiration = null;
        String expirationAsString = name.substring(separatorIndex + 1);
        try {
            // calculate expiration, support arithmetic expressions.
            if(pattern.matcher(expirationAsString).find()){
                expiration = (long) Double.parseDouble(scriptEngine.eval(expirationAsString).toString());
            }else{
                expiration = Long.parseLong(expirationAsString);
            }
        } catch (NumberFormatException ex) {
            logger.error(String.format("Cannnot separate expiration time from cache: '%s'", name), ex);
        } catch (ScriptException e) {
            logger.error(String.format("Cannnot separate expiration time from cache: '%s'", name), e);
        }

        return expiration;
    }

    @Override
    public void setUsePrefix(boolean usePrefix) {
        super.setUsePrefix(usePrefix);
    }

    @Override
    public void setCachePrefix(RedisCachePrefix cachePrefix) {
        super.setCachePrefix(cachePrefix);
    }

    public void setDefaultCacheName(String defaultCacheName) {
        this.defaultCacheName = defaultCacheName;
    }
}

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:cache="http://www.springframework.org/schema/cache"
       xmlns:p="http://www.springframework.org/schema/p"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
         http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache-4.0.xsd">

    <!-- Jedis PoolConfig -->
    <bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
        <property name="maxTotal" value="${redis.pool.maxTotal}"/>
        <property name="maxIdle" value="${redis.pool.maxIdle}"/>
        <property name="minIdle" value="${redis.pool.minIdle}"/>
        <property name="maxWaitMillis" value="${redis.pool.maxWait}"/>
        <!--对拿到的connection进行validateObject校验-->
        <property name="testOnBorrow" value="${redis.pool.testOnBorrow}"/>
        <!--在进行returnObject对返回的connection进行validateObject校验-->
        <property name="testOnReturn" value="${redis.pool.testOnReturn}"/>
        <!--定时对线程池中空闲的链接进行validateObject校验-->
        <property name="testWhileIdle" value="${redis.pool.testWhileIdle}"/>
    </bean>

    <!-- Redis SentinelConfig-->
    <bean id="redisSentinelConfiguration" class="org.springframework.data.redis.connection.RedisSentinelConfiguration">
        <property name="master">
            <bean class="org.springframework.data.redis.connection.RedisNode">
                <property name="name" value="mymaster"/>
            </bean>
        </property>
        <property name="sentinels">
            <set>
                <bean class="org.springframework.data.redis.connection.RedisNode">
                    <constructor-arg name="host" value="${redis.sentinel1.host}"></constructor-arg>
                    <constructor-arg name="port" value="${redis.sentinel1.port}"></constructor-arg>
                </bean>
                <bean class="org.springframework.data.redis.connection.RedisNode">
                    <constructor-arg name="host" value="${redis.sentinel2.host}"></constructor-arg>
                    <constructor-arg name="port" value="${redis.sentinel2.port}"></constructor-arg>
                </bean>
            </set>
        </property>
    </bean>

    <!-- Jedis ConnectionFactory -->
    <bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
        <constructor-arg name="sentinelConfig" ref="redisSentinelConfiguration"></constructor-arg>
        <constructor-arg name="poolConfig" ref="jedisPoolConfig"></constructor-arg>
    </bean>

    <!-- Redis Template -->
    <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
        <property name="connectionFactory" ref="jedisConnectionFactory"/>
        <property name="keySerializer">
            <bean class="org.springframework.data.redis.serializer.StringRedisSerializer"/>
        </property>
    </bean>

    <!-- 开启缓存注解 -->
    <cache:annotation-driven/>

    <!-- 自定义CacheManager,实现缓存有效期可配置 -->
    <bean name="cacheManager" class="com.caiya.cache.redis.ExtendedRedisCacheManager">
        <constructor-arg name="redisOperations" ref="redisTemplate"/>
        <constructor-arg name="cacheNames">
            <set>
                <value>caiya_a</value>
                <value>caiya_test</value>
            </set>
        </constructor-arg>
        <!-- 默认缓存名字 -->
        <property name="defaultCacheName" value="caiya_a"/>
        <!-- 是否在容器启动时初始化 -->
        <property name="loadRemoteCachesOnStartup" value="true"/>
        <!-- 是否使用前缀 -->
        <property name="usePrefix" value="true"/>
        <!-- 前缀命名,仅当usePrefix为true时才生效 -->
        <property name="cachePrefix">
            <bean class="org.springframework.data.redis.cache.DefaultRedisCachePrefix">
                <constructor-arg name="delimiter" value=":"/>
            </bean>
        </property>
        <!-- 缓存名字和有效期的分隔符 -->
        <property name="separator" value="#"/>
        <!-- 默认有效期1h -->
        <property name="defaultExpiration" value="3600"/>
        <!-- 多个缓存有效期,一般的单个工程可以省略此项 -->
        <property name="expires">
            <map>
                <entry key="caiya_a" value="1800"/>
            </map>
        </property>
    </bean>

</beans>

注解使用(注意key的生成策略):

/**
 * @description 本方法在redis实例中的key为:"caiya_a:querySoftwareList_null_null_null_null_1_10",默认会将cacheName追加在最前面.
 * 查询软件列表
 * @param cid 类目ID
 * @param origin 来源
 * @param sortField 排序字段
 * @param sortType 排序顺序
 * @param pageNo 当前页
 * @param pageSize 每页大小
 * @return 软件包装器
 */
@Cacheable(value = "caiya_a#(60 * 10)", key = "'querySoftwareList_' + #cid + '_' + #origin + '_' + #sortField + '_' + #sortType + '_' + #pageNo + '_' + #pageSize")
public SoftwareWrapper querySoftwareList(Long cid, String origin, String sortField, String sortType, Integer pageNo, Integer pageSize) {
   ......
   return ...;
}

关于spring的spEL表达式可参考:

http://docs.spring.io/spring-framework/docs/4.1.x/spring-framework-reference/html/cache.html
http://stackoverflow.com/questions/14072380/cacheable-key-on-multiple-method-arguments

 

展开阅读全文
打赏
2
16 收藏
分享
加载中
菜蚜博主

引用来自“勤奋的羊羊”的评论

感觉还是有点麻烦,有木有更方便的,比如在注解@Cacheable里面设置有效期

回复@勤奋的羊羊 : 不扩展,就没得呀。这个其实挺简单,只是重写一下CacheManager。
2017/12/06 09:37
回复
举报
感觉还是有点麻烦,有木有更方便的,比如在注解@Cacheable里面设置有效期
2017/10/25 17:43
回复
举报
菜蚜博主

引用来自“Dreamlu”的评论

RedisCacheManager里面有个
private Map<String, Long> expires = null;
配置下cacheName的超时就好了另外还有defaultExpiration,你这整复杂了。
但是我的cacheName是比较少的,缓存有效期都不全是相同的。某种需要会遍历key,或者查看key。
2016/03/30 17:31
回复
举报
RedisCacheManager里面有个
private Map<String, Long> expires = null;
配置下cacheName的超时就好了另外还有defaultExpiration,你这整复杂了。
2016/03/30 13:37
回复
举报
更多评论
打赏
4 评论
16 收藏
2
分享
返回顶部
顶部