zoukankan      html  css  js  c++  java
  • spring aop搭建redis缓存

    SpringAOP与Redis搭建缓存

    近期项目查询数据库太慢,持久层也没有开启二级缓存,现希望采用Redis作为缓存。为了不改写原来代码,在此采用AOP+Redis实现。

    目前由于项目需要,只需要做查询部分:

    数据查询时每次都需要从数据库查询数据,数据库压力很大,查询速度慢,因此设置缓存层,查询数据时先从redis中查询,如果查询不到,则到数据库中查询,然后将数据库中查询的数据放到redis中一份,下次查询时就能直接从redis中查到,不需要查询数据库了。

    redis作为缓存的优势:

    1.内存级别缓存,查询速度毋庸置疑。

    2.高性能的K-V存储系统,支持String,Hash,List,Set,Sorted Set等数据类型,能够应用在很多场景中。

    3.redis3.0版本以上支持集群部署。

    4.redis支持数据的持久化,AOF,RDB方式。

    实体类与表:

    复制代码
    public class RiskNote implements Serializable {
    
        private static final long serialVersionUID = 4758331879028183605L;
        
        private Integer ApplId;
        private Integer allqyorg3monNum;
        private Double loanF6endAmt;
        
        private String isHighRisk1;
        private Date createDate;
        private String risk1Detail;
        
        private Integer risk2;
        private String risk3;
        private String creditpaymonth;
            
        ......
    复制代码

    Redis与Spring集成参数:

    redis.properties

    复制代码
    #redis settings
    redis.minIdle=5
    redis.maxIdle=10
    redis.maxTotal=50
    redis.maxWaitMillis=1500
    redis.testOnBorrow=true
    redis.numTestsPerEvictionRun=1024
    redis.timeBetweenEvictionRunsMillis=30000
    redis.minEvictableIdleTimeMillis=1800000
    redis.softMinEvictableIdleTimeMillis=10000
    redis.testWhileIdle=true
    redis.blockWhenExhausted=false
    
    #redisConnectionFactory settings
    redis.host=192.168.200.128
    redis.port=6379
    复制代码

    集成配置文件:applicationContext_redis.xml

    复制代码
        <!-- 加载配置数据 -->
        <bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
            <property name="systemPropertiesModeName" value="SYSTEM_PROPERTIES_MODE_OVERRIDE" />
            <property name="ignoreResourceNotFound" value="true" />
            <property name="locations">
                <list>
                    <value>classpath*:/redis.properties</value>
                </list>
            </property>
        </bean>
    
        <!-- 注解扫描 -->
        <context:component-scan base-package="com.club.common.redis"/>
    
        <!-- jedis连接池配置 -->
        <bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig">
            <!-- 最小空闲连接数 -->  
             <property name="minIdle" value="${redis.minIdle}"/>
             <!-- 最大空闲连接数 -->
            <property name="maxIdle" value="${redis.maxIdle}"/>
            <!-- 最大连接数 -->    
            <property name="maxTotal" value="${redis.maxTotal}"/>
            <!-- 获取连接时的最大等待毫秒数,小于零:阻塞不确定的时间,默认-1 -->    
            <property name="maxWaitMillis" value="${redis.maxWaitMillis}"/>
            <!-- 在获取连接的时候检查有效性, 默认false -->   
            <property name="testOnBorrow" value="${redis.testOnBorrow}"/>
            <!-- 每次释放连接的最大数目 -->
            <property name="numTestsPerEvictionRun" value="${redis.numTestsPerEvictionRun}"/>
            <!-- 释放连接的扫描间隔(毫秒) -->
            <property name="timeBetweenEvictionRunsMillis" value="${redis.timeBetweenEvictionRunsMillis}"/>
            <!-- 连接最小空闲时间 -->
            <property name="minEvictableIdleTimeMillis" value="${redis.minEvictableIdleTimeMillis}"/>
            <!-- 连接空闲多久后释放, 当空闲时间>该值 且 空闲连接>最大空闲连接数 时直接释放 -->
            <property name="softMinEvictableIdleTimeMillis" value="${redis.softMinEvictableIdleTimeMillis}"/>
            <!-- 在空闲时检查有效性, 默认false -->
            <property name="testWhileIdle" value="${redis.testWhileIdle}"/>
            <!-- 连接耗尽时是否阻塞, false报异常,ture阻塞直到超时, 默认true -->
            <property name="blockWhenExhausted" value="${redis.blockWhenExhausted}"/>   
        </bean>    
        
        <!-- redis连接池 -->
        <bean id="jedisPool" class="redis.clients.jedis.JedisPool" destroy-method="close">
            <constructor-arg name="poolConfig" ref="poolConfig"/>
            <constructor-arg name="host" value="${redis.host}"/>
            <constructor-arg name="port" value="${redis.port}"/>
        </bean>
        
        <bean id="redisCache" class="com.club.common.redis.RedisCache">
            <property name="jedisPool" ref="jedisPool"></property>
        </bean>
         
        <bean id="testDao" class="com.club.common.redis.TestDao"></bean>
        <bean id="testService" class="com.club.common.redis.service.TestService"></bean>
        
        <!-- 开启Aspect切面支持 -->
        <aop:aspectj-autoproxy/>
    
    </beans>  
    复制代码

    测试,所以各层级没有写接口。

    DAO层查询数据,封装对象:

    复制代码
    public class TestDao {
        
        //查询
        public RiskNote getByApplId(Integer applId) throws Exception{
            
            Class.forName("oracle.jdbc.driver.OracleDriver");    
            Connection connection = DriverManager.getConnection("jdbc:oracle:thin:@192.168.11.215:1521:MFTEST01", "datacenter", "datacenter");
            PreparedStatement statement = connection.prepareStatement("select * from  TEMP_RISK_NOTE where appl_id=?");
            
            //执行
            statement.setInt(1, applId);
            ResultSet resultSet = statement.executeQuery();
            
            RiskNote riskNote = new RiskNote();
            //解析
            while (resultSet.next()) {
                riskNote.setApplId(resultSet.getInt("APPL_ID"));
                riskNote.setAllqyorg3monNum(resultSet.getInt("ALLQYORG3MON_NUM"));
                riskNote.setLoanF6endAmt(resultSet.getDouble("LOAN_F6END_AMT"));
                riskNote.setIsHighRisk1(resultSet.getString("IS_HIGH_RISK_1"));
                riskNote.setCreateDate(resultSet.getDate("CREATE_DATE"));
                riskNote.setRisk1Detail(resultSet.getString("RISK1_DETAIL"));
                riskNote.setRisk2(resultSet.getInt("RISK2"));
                riskNote.setRisk3(resultSet.getString("RISK3"));
                riskNote.setCreditpaymonth(resultSet.getString("CREDITPAYMONTH"));
                
            }
            
            return riskNote;
        }
    }
    复制代码

    Service层调用DAO:

    复制代码
    @Service
    public class TestService {
        
        @Autowired
        private TestDao testDao;
        
        public Object get(Integer applId) throws Exception{
            
            RiskNote riskNote = testDao.getByApplId(applId);
            
            return riskNote;
            
        }
    }
    复制代码

    测试:

    复制代码
    public class TestQueryRiskNote {
        
        
        @Test
        public void testQuery() throws Exception{
            ApplicationContext ac = new FileSystemXmlApplicationContext("src/main/resources/spring/applicationContext_redis.xml");
            TestService testService = (TestService) ac.getBean("testService");
            RiskNote riskNote = (RiskNote)testService.get(91193);
            System.out.println(riskNote);
        }
    }
    复制代码

    此时测试代码输出的是查询到的RiskNote对象,可以重写toString方法查看

    结果如下:最后输出的对象

    在虚拟机Linux系统上搭建Redis,具体教程请自行百度

    redis支持多种数据结构,查询的对象可以直接使用hash结构存入redis。

    因为项目中各个方法查询的数据不一致,比如有简单对象,有List集合,有Map集合,List中套Map套对象等复杂结构,为了实现统一性和通用性,redis中也刚好提供了set(byte[],byte[])方法,所以可以将对象序列化后存入redis,取出后反序列化为对象。

    序列化与反序列化工具类:

    复制代码
    /**
     * 
     * @Description: 序列化反序列化工具
     */
    public class SerializeUtil {
        /**
         * 
         * 序列化
         */
        public static byte[] serialize(Object obj){
            
            ObjectOutputStream oos = null;
            ByteArrayOutputStream baos = null;
            
            try {
                //序列化
                baos = new ByteArrayOutputStream();
                oos = new ObjectOutputStream(baos);
                
                oos.writeObject(obj);
                byte[] byteArray = baos.toByteArray();
                return byteArray;
                
            } catch (IOException e) {
                e.printStackTrace();
            }    
            return null;
        }
        
        /**
         * 
         * 反序列化
         * @param bytes
         * @return
         */
        public static Object unSerialize(byte[] bytes){
            
            ByteArrayInputStream bais = null;
            
            try {
                //反序列化为对象
                bais = new ByteArrayInputStream(bytes);
                ObjectInputStream ois = new ObjectInputStream(bais);
                return ois.readObject();
                
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }
    }
    复制代码

    切面分析:

    切面:查询前先查询redis,如果查询不到穿透到数据库,从数据库查询到数据后,保存到redis,然后下次查询可直接命中缓存

    目标方法是查询数据库,查询之前需要查询redis,这是前置

    假设从redis中没有查到,则查询数据库,执行完目标方法后,需要将查询的数据放到redis以便下次查询时不需要再到数据库中查,这是后置

    所以,可以将切面中的通知定为环绕通知

    切面类编写如下:

    复制代码
    /**
     * @Description: 切面:查询前先查询redis,如果查询不到穿透到数据库,从数据库查询到数据后,保存到redis,然后下次查询可直接命中缓存
     */
    @Component
    @Aspect
    public class RedisAspect {
    
        @Autowired
        @Qualifier("redisCache")
        private RedisCache redisCache;
        
        //设置切点:使用xml,在xml中配置
        @Pointcut("execution(* com.club.common.redis.service.TestService.get(java.lang.Integer)) and args(applId)")    //测试用,这里还额外指定了方法名称,方法参数类型,方法形参等,比较完整的切点表达式
       public void myPointCut(){ } @Around("myPointCut()") public Object around(ProceedingJoinPoint joinPoint){ //前置:到redis中查询缓存 System.out.println("调用从redis中查询的方法..."); //先获取目标方法参数 String applId = null; Object[] args = joinPoint.getArgs(); if (args != null && args.length > 0) { applId = String.valueOf(args[0]); } //redis中key格式: applId String redisKey = applId; //获取从redis中查询到的对象 Object objectFromRedis = redisCache.getDataFromRedis(redisKey); //如果查询到了 if(null != objectFromRedis){ System.out.println("从redis中查询到了数据...不需要查询数据库"); return objectFromRedis; } System.out.println("没有从redis中查到数据..."); //没有查到,那么查询数据库 Object object = null; try { object = joinPoint.proceed(); } catch (Throwable e) { e.printStackTrace(); } System.out.println("从数据库中查询的数据..."); //后置:将数据库中查询的数据放到redis中 System.out.println("调用把数据库查询的数据存储到redis中的方法..."); redisCache.setDataToRedis(redisKey, object); //将查询到的数据返回 return object; } }
    复制代码

    从redis中查询数据,以及将数据库查询的数据保存到redis的方法:

    复制代码
    /**
     * 
     * @Description:Redis缓存
     */
    public class RedisCache {
        
        @Resource
        private JedisPool jedisPool;
        public JedisPool getJedisPool() {
            return jedisPool;
        }
        public void setJedisPool(JedisPool jedisPool) {
            this.jedisPool = jedisPool;
        }
    
        //从redis缓存中查询,反序列化
        public Object getDataFromRedis(String redisKey){
            //查询
            Jedis jedis = jedisPool.getResource();
            byte[] result = jedis.get(redisKey.getBytes());
            
            //如果查询没有为空
            if(null == result){
                return null;
            }
            
            //查询到了,反序列化
            return SerializeUtil.unSerialize(result);
        }
        
        //将数据库中查询到的数据放入redis
        public void setDataToRedis(String redisKey, Object obj){
            
            //序列化
            byte[] bytes = SerializeUtil.serialize(obj);
            
            //存入redis
            Jedis jedis = jedisPool.getResource();
            String success = jedis.set(redisKey.getBytes(), bytes);
            
            if("OK".equals(success)){
                System.out.println("数据成功保存到redis...");
            }
        }
    }
    复制代码

    测试1:此时redis中没有查询对象的数据

    结果是:先到redis中查询,没有查到数据,然后代理执行从数据库中查询,然后把数据存入到redis中一份,那么下次查询就可以直接从redis中查询了

    测试2:此时redis中已经有上一次从数据库中查询的数据了

    在项目中测试后:效果还是非常明显的,有一个超级复杂的查询,格式化之后的sql是688行,每次刷新页面都需要重新查询,耗时10秒左右。

    在第一次查询放到redis之后,从redis中查询能够在2秒内得到结果,速度非常快。

    上面的是在项目改造前写的一个Demo,实际项目复杂的多,切点表达式是有两三个一起组成的,也着重研究了一下切点表达式的写法

    如:

    @Pointcut("(execution(* com.club.risk.center.service.impl.*.*(java.lang.String))) || (execution(* com.club.risk.py.service.impl.PyServcieImpl.queryPyReportByApplId(java.lang.String))) || (execution(* com.club.risk.zengxintong.service.Impl.ZXTServiceImpl.queryZxtReportByApplId(..)))")

    这是多个切点组合形成使用||连接。

    我在实际项目中使用的key也比applId复杂,因为可能只使用applId的话导致key冲突,

    所以项目中使用的key是applId:方法全限定名,,这样的话key能够保证是一定不一致的。

    如下:

    复制代码
        //先获取目标方法参数
            String applId = null;
            Object[] args = joinPoint.getArgs();
            if (args != null && args.length > 0) {
               applId = String.valueOf(args[0]);
            }
            
            //获取目标方法所在类
            String target = joinPoint.getTarget().toString();
            String className = target.split("@")[0];
            
            //获取目标方法的方法名称
            String methodName = joinPoint.getSignature().getName();
            
            //redis中key格式:    applId:方法名称
            String redisKey = applId + ":" + className + "." + methodName;
    复制代码

    所以上面的是一种通用的处理,具体到项目中还要看具体情况。

    以前没有自己写过AOP代码,这次使用突然发现AOP确实强大,在整个过程中除了配置文件我没有改任何以前的源代码,功能全部是切入进去的。

    这个Demo也基本上实现了需求,只需要设置切点,能够将缓存应用到各种查询方法中,或设置切点为service.impl包,直接作用于所有service方法。

  • 相关阅读:
    Linux zip打包排除某个目录或只打包某个目录
    解决coreseek及sphinx查询结果不全--匹配参数详解
    curl错误28:Resolving timed out after 15009 milliseconds解决方案
    PHP实现关键词全文搜索Sphinx及中文分词Coreseek的安装配置
    Nginx指定多个域名跨域配置
    PHP友盟推送消息踩坑及处理
    Redis批量删除的方法
    Redis数据类型及常用方法整理
    PHPExcel导入导出常用方法总结
    [633] 平方数之和
  • 原文地址:https://www.cnblogs.com/jianwei-dai/p/5910469.html
Copyright © 2011-2022 走看看