Spring集成Redis实现缓存实践

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/iverson3sod/article/details/52450080

为什么要使用缓存

在我们所编写的应用中,很多请求总是会一遍遍地去获取一些相同的数据,因为这些数据是无状态的,所以当请求任务完成后,就会立马丢掉所获取的数据,在这些数据中,有些是需要时间去数据库获取,或者远程接口调用获取,或执行复杂运算得到。如果这部分数据变化不那么频繁,或者压根不会变化,那我们何必要每次都去加载呢。
在web应用中,缓存是一门必备的技能,只要有需要优化的地方,我们首先会想到的就是使用缓存来提升性能。缓存技术也随着web技术的不断进步而不断演变,从本地缓存,到集群缓存、分布式缓存。

常用的缓存框架

常用的缓存框架有:Ehcache、Redis、Memcache等,这些框架都能帮助我们很好的实现数据缓存,具体各个框架的区别可参考:URL,这里我使用的是Redis3.2框架,并使用 Redis 官方首选的 Java 客户端开发包Jedis2.8。

Redis安装配置

Redis的安装很简单,网上有很多教程,这里说下配置:
1、 redis.conf配置文件,Redis启动的时候需要一个redis.conf配置文件,主要有以下配置项:
这里写图片描述
这里写图片描述
这里写图片描述

2、 单机多实例
cp redis.conf,命名为redis6380.conf;
vim redis6380.conf,编辑以下几个参数:port : 6380;pidfile : /var/run/redis6380.pid;
启动:redis-server /user/local/etc/redis.conf,redis-server /user/local/etc/redis6380.conf,分别启动两个redis实例,查看是否启动:ps –ef|grpe redis
这里写图片描述

3、 主从配置
Redis的主从配置和切换非常简单,只要在slave实例中配置slaveof 主服务IP 主服务端口 :slaveof 192.168.1.106 6379 ,如果主服务设置了密码,配置 masterauth 主服务密码:masterauth newdoone
4、 集群配置
暂未实践

Spring集成Jedis配置

Redis服务安装好并启动后,我们需要通过Jedis客户端来连接并操作Redis实现数据缓存。我们使用Spring集成Jedis。由于Jedis的版本差异,Spring集成Jedis的配置会存在差异,如不同版本属性存在差异,属性名称可能也不一样。我们安装Redis最新版本3.2,Jedis使用2.8.1版本,Spring使用最新4.3版本。以下介绍几种配置方式:

1、涉及jar:
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述

2、redis连接配置:

# Redis settings
#redis.host=192.168.1.106
redis.host=134.64.42.14
redis.port=6379
redis.pass=newdoone
redis.timeout=2000
#redis2实例
redis.host2=134.64.42.30
#redis.host2=192.168.1.106
redis.port2=6380
redis.pass2=newdoone
redis.timeout2=2000

#一个pool最多有多少个状态为idle(空闲)的jedis实例
redis.maxIdle=300
#一个pool可分配多少个jedis实例
redis.maxActive=2048
#当borrow一个jedis实例时,最大的等待时间,如果超过等待时间,则直接抛出JedisConnectionException
redis.maxWait=1500
#在borrow一个jedis实例时,是否提前进行validate操作;如果为true,则得到的jedis实例均是可用的
redis.testOnBorrow=true
#连接耗尽时是否阻塞, false报异常,ture阻塞直到超时, 默认true
redis.blockWhenExhausted=false
redis.testOnReturn=false

3、Spring集成单机配置:
如果是单台服务器单个Redis实例,可使用以下配置,在需要操作的地方注入RedisTemplate进行操作,可直接set、get对象和设置有效期等。

    <!-- 单机配置  BENGIN-->
    <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="connectionFactory"
        class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
        <property name="poolConfig" ref="poolConfig" />
        <property name="port" value="${redis.port}" />
        <property name="hostName" value="${redis.host}" />
        <property name="password" value="${redis.password}" />
        <property name="timeout" value="${redis.timeout}"></property>
    </bean>
    <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
        <property name="connectionFactory" ref="connectionFactory" />
        <property name="keySerializer">
            <bean
                class="org.springframework.data.redis.serializer.StringRedisSerializer" />
        </property>
        <property name="valueSerializer">
            <bean
                class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer" />
        </property>
    </bean>
   <!-- 单机配置  END-->

4、Spring集成多机配置:
有时一台Redis服务未必能满足扩张的业务需求,而Redis在3.x版本前是不支持服务端集群的,所以Jedis通过采用一致性哈稀分片算法(Shard),将不同的key分配到不同的redis server上,以达到横向扩展的目的。

<!-- 分片池  BEGIN-->
   <bean id="shardedJedisPool" class="redis.clients.jedis.ShardedJedisPool">
        <constructor-arg index="0" ref="jedisPoolConfig"/>
        <constructor-arg index="1">
            <list>
                <bean class="redis.clients.jedis.JedisShardInfo">
                    <constructor-arg index="0" value="${redis.host}"/>    
                    <constructor-arg index="1" value="${redis.port}" type="int"/>
                    <constructor-arg index="2" value="${redis.timeout}" type="int"/>
                    <property name="password" value="${redis.pass}"/>
                </bean>
                <bean class="redis.clients.jedis.JedisShardInfo">
                    <constructor-arg index="0" value="${redis.host2}"/>       
                    <constructor-arg index="1" value="${redis.port2}" type="int"/>
                    <constructor-arg index="2" value="${redis.timeout2}" type="int"/>
                    <property name="password" value="${redis.pass2}"/>
                </bean>
            </list>
        </constructor-arg>
    </bean>
    <!-- 连接池配置 最大空闲数、最大连接数、最长等待时间、连接是否可用 -->  
    <bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
        <property name="maxTotal" value="${redis.maxActive}" />
        <property name="maxIdle" value="${redis.maxIdle}" />
        <property name="maxWaitMillis" value="${redis.maxWait}"/>
        <property name="testOnBorrow" value="${redis.testOnBorrow}" />
        <property name="testOnReturn" value="${redis.testOnReturn}"/>
        <property name="blockWhenExhausted" value="${redis.blockWhenExhausted}"/>
    </bean>
    <!-- 分片池  END-->

5、redis3.x集群配置:
Redis3.x开始支持服务端集群配置,在安装完Redis服务后,需要在服务端进行集群配置,再通过Spring配置加载jedisCluster,在需要的地方注入jedisCluster完成操作。由于暂未实践此场景,这里就不作介绍了。

Spring基于注解的缓存技术

Spring在3.1版本中引入了基于注解的缓存技术Spring Cache,它本质上不是一个具体的缓存实现方案(例如EHCache 或者 OSCache),而是一个对缓存使用的抽象,通过在既有代码中添加少量它定义的各种 annotation,即能够达到缓存方法的返回对象的效果。其特点总结如下:
• 通过少量的配置 annotation 注释即可使得既有代码支持缓存
• 支持开箱即用 Out-Of-The-Box,即不用安装和部署额外第三方组件即可使用缓存
• 支持 Spring Express Language,能使用对象的任何属性或者方法来定义缓存的 key 和 condition
• 支持 AspectJ,并通过其实现任何方法的缓存支持
• 支持自定义 key 和自定义缓存管理者,具有相当的灵活性和扩展性
使用方法也很简单,在第四步的配置中增加以下配置:

<!-- 开启spring cache注解功能-->
<cache:annotation-driven cache-manager="redisCacheManager"/>
<!-- Spring Cache缓存管理器 BEGIN -->
<bean id="cacheManager" class="org.springframework.data.redis.cache.RedisCacheManager">
    <constructor-arg name="template" ref="redisTemplate" />
</bean>

然后在方法上增加@Cacheable、@CachePut、@CacheEvict注解,可以轻松的实现缓存,但有一个弊端,貌似不能针对不同的业务自定义有效期。

自定义注解的Spring AspectJ AOP的缓存实现

【参考:http://blog.csdn.net/zhanngle/article/details/41077423

在上面第四步中,我们通过Spring的集成配置,可以获取到操作Redis的相关对象,如:RedisTemplate、shardedJedisPool、jedisCluster,接下来就是使用这些对象进行缓存操作了,我们使用自定义注解,通过shardedJedisPool对象操作实现多个Redis服务的扩展使用:
1、 操作工具类
点击下载
2、 序列化和反序列化工具类
由于shardedJedisPool原子接口只能set/get String类型,我们通过序列化工具来实现对对象的set和get,使用Protobuff插件序列化/反序列化

/**
 * 
 */
package com.doone.commplat.utils.serialize;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.springframework.objenesis.Objenesis;
import org.springframework.objenesis.ObjenesisStd;

import com.dyuproject.protostuff.LinkedBuffer;
import com.dyuproject.protostuff.ProtobufIOUtil;
import com.dyuproject.protostuff.Schema;
import com.dyuproject.protostuff.runtime.RuntimeSchema;

/**
 * @author: pengl
 * @Date:2016年7月22日 下午2:55:11
 * @Description:Protobuff插件序列化
 */
public class ProtobuffSerializationUtil {
    private static Map<Class<?>, Schema<?>> cachedSchema = new ConcurrentHashMap<Class<?>, Schema<?>>();
    private static Objenesis objenesis = new ObjenesisStd(true);

    private static <T> Schema<T> getSchema(Class<T> clazz) {
        @SuppressWarnings("unchecked")
        Schema<T> schema = (Schema<T>) cachedSchema.get(clazz);
        if (schema == null) {
            schema = RuntimeSchema.getSchema(clazz);
            if (schema != null) {
                cachedSchema.put(clazz, schema);
            }
        }
        return schema;
    }

    /**
     * 序列化
     *
     * @param obj
     * @return
     */
    public static <T> byte[] serializer(T obj) {
        @SuppressWarnings("unchecked")
        Class<T> clazz = (Class<T>) obj.getClass();
        LinkedBuffer buffer = LinkedBuffer
                .allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE);
        try {
            Schema<T> schema = getSchema(clazz);
            return ProtobufIOUtil.toByteArray(obj, schema, buffer);
        } catch (Exception e) {
            throw new IllegalStateException(e.getMessage(), e);
        } finally {
            buffer.clear();
        }
    }

    /**
     * 反序列化
     *
     * @param data
     * @param clazz
     * @return
     */
    public static <T> T deserializer(byte[] data, Class<T> clazz) {
        try {
            T obj = objenesis.newInstance(clazz);
            Schema<T> schema = getSchema(clazz);
            ProtobufIOUtil.mergeFrom(data, obj, schema);
            return obj;
        } catch (Exception e) {
            throw new IllegalStateException(e.getMessage(), e);
        }
    }
}

3、 自定义注解(RedisCacheable/ RedisCacheKey)

/**
 * 
 */
package com.doone.commplat.utils.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @author: pengl
 * @Date:2016年7月11日 下午3:52:04 
 * @Description:自定义Redis缓存集成注解
 */
@Retention(RetentionPolicy.RUNTIME)  
@Target({ElementType.METHOD})  
public @interface RedisCacheable {
    public enum KeyMode{  
        DEFAULT,    //只有加了@CacheKey的参数,才加入key后缀中  
        BASIC,      //只有基本类型参数,才加入key后缀中,如:String,Integer,Long,Short,Boolean  
        ALL,        //所有参数都加入key后缀  
        BEAN,       //bean的属性加入KEY后缀
        MAP;        //Map的属性加入KEY后缀
    }  

    public String key() default "";     //缓存key  
    public KeyMode keyMode() default KeyMode.DEFAULT;       //key的后缀模式  
    public int expire() default 7200;      //缓存多少秒,默认2个小时
}
/**
 * 
 */
package com.doone.commplat.utils.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @author: pengl
 * @Date:2016年7月11日 下午4:00:23 
 * @Description:
 */
@Retention(RetentionPolicy.RUNTIME)  
@Target({ElementType.PARAMETER}) 
public @interface RedisCacheKey {

}

4、 创建Aop拦截器的处理类

/**
 * 
 */
package com.doone.commplat.utils.aop;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.Map;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.ReflectionUtils;

import com.doone.commplat.utils.annotation.RedisCacheKey;
import com.doone.commplat.utils.annotation.RedisCacheable;
import com.doone.commplat.utils.annotation.RedisCacheable.KeyMode;
import com.doone.commplat.utils.redis.RedisUtil;

/**
 * @author: pengl
 * @Date:2016年7月11日 下午3:53:15 
 * @Description:拦截@RedisCacheable注解
 */
@Aspect
@Component
public class RedisCacheableAspect {
    @Autowired
    private RedisUtil redisUtil; 

    @Around("@annotation(cache)")  
    public Object cached(final ProceedingJoinPoint pjp,RedisCacheable cache) throws Throwable {  

        String key = getCacheKey(pjp, cache);
        String signature = pjp.getSignature().toLongString();
        String returnType = signature.split(" ")[1]; 
        Object value = redisUtil.get(key,Class.forName(returnType));    //从缓存获取数据  
        if(value != null) {
            return value;
        }       
        value = pjp.proceed();      //跳过缓存,到后端查询数据

        if(value == null)
            return value;

        if(cache.expire()<=0) {      //如果没有设置过期时间,则无限期缓存  
            redisUtil.set(key, value);  
        } else {                    //否则设置缓存时间  
            redisUtil.set(key, value, cache.expire());  
        }  
        return value;  
    }  

    /** 
     * 获取缓存的key值 
     * @param pjp 
     * @param cache 
     * @return 
     */  
    private String getCacheKey(ProceedingJoinPoint pjp,RedisCacheable cache) {  

        StringBuilder buf=new StringBuilder();  
        buf.append(pjp.getSignature().getDeclaringTypeName()).append(".").append(pjp.getSignature().getName());  
        String keyStr = cache.key();
        if(!(cache.keyMode()==KeyMode.BEAN)){
            if(keyStr.length()>0) {  
                buf.append(".").append(keyStr);  
            }  

        }

        Object[] args=pjp.getArgs();  
        if(cache.keyMode()==KeyMode.DEFAULT) {  
            Annotation[][] pas=((MethodSignature)pjp.getSignature()).getMethod().getParameterAnnotations();  
            for(int i=0;i<pas.length;i++) {  
                for(Annotation an:pas[i]) {  
                    if(an instanceof RedisCacheKey) {  
                        buf.append(".").append(args[i].toString());  
                        break;  
                    }  
                }  
            }  
        } else if(cache.keyMode()==KeyMode.BASIC) {  
            for(Object arg:args) {  
                if(arg instanceof String) {  
                    buf.append(".").append(arg);  
                } else if(arg instanceof Integer || arg instanceof Long || arg instanceof Short) {  
                    buf.append(".").append(arg.toString());  
                } else if(arg instanceof Boolean) {  
                    buf.append(".").append(arg.toString());  
                }  
            }  
        } else if(cache.keyMode()==KeyMode.ALL) {  
            for(Object arg:args) {  
                buf.append(".").append(arg.toString());  
            }  
        } else if(cache.keyMode()==KeyMode.BEAN) {  
            if(keyStr.length()>0) {  
                Object target = args[0];
                Field field = ReflectionUtils.findField(target.getClass(), keyStr);
                ReflectionUtils.makeAccessible(field);
                Object value = ReflectionUtils.getField(field, target);
                buf.append("#").append(value);  
            }  
        } else if(cache.keyMode()==KeyMode.MAP) {  
            if(keyStr.length()>0) {  
                Map target = (Map)args[0];
                buf.append("#").append(target.get(keyStr));  
            }  
        } 

        return buf.toString();  
    }
}

5、 测试
编写一个从数据库获取数据的方法,使用自定义的缓存注解:

@RedisCacheable(expire=300,keyMode=RedisCacheable.KeyMode.BEAN,key="logid")
    public ExceptionLogBean getLogInfo2(ExceptionLogBean exceptionLogBean){
        return exceptionLogService.selectLogByLogid(exceptionLogBean);
    }
测试方法:
@org.junit.Test
    public void testRedis2() throws UnsupportedEncodingException{
        RedisDemo redisDemo = (RedisDemo)SpringBeanUtil.getBean("redisDemo");
        ExceptionLogBean par = new ExceptionLogBean();
        par.setLogid(4);
        ExceptionLogBean exceptionLogBean = redisDemo.getLogInfo2(par);
        _logger.info(dateUtil.parseDate(exceptionLogBean.getCreatedate(),"yyyy-MM-dd HH:mm:ss"));
    }
通过控制台打印的SQL日志可以看到,第一次执行时,是从数据库查询获取,这时我们查看通过redis-cli查看KEY *,端口为6379的redis实例中出现了缓存:

这里写图片描述
这里写图片描述

再执行一次,控制台未打印SQL日志,直接打印了结果值,说明是从缓存中获取的值,再将logid分别改为1,2,3执行三次,通过redis-cli可以看到两个实例分别存储了两个缓存:

这里写图片描述
这里写图片描述

猜你喜欢

转载自blog.csdn.net/iverson3sod/article/details/52450080