springmvc+mybatis+redis二级缓存实现详解

最近项目改版,准备重构一下项目的架构,于是想到了之前项目存在的短板,在页面经常访问的时候造成网络带宽浪费,对于也,页面刷新导致的频繁操作数据库想到了用mybatis的二级缓存,以此可以大大提高访问速度。
经过一番折腾,发现可以用两种方式来实现,一种是直接在mapper.xml里面配置cache开启二级缓存,另外一种是通过spring的注解@Cacheable来实现缓存。

准备工作:

部署一台redis并且启动,可以是本地,也可以是远程服务器。

一、基于mapper.xml方式

1.所需要的依赖
        <!-- redis客户端  jedis -->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>2.8.0</version>
        </dependency>
        <!-- spring-redis实现 -->
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-redis</artifactId>
            <version>1.6.2.RELEASE</version>
        </dependency>
2.配置redis.properties
#redis settings
redis.host= 你的host ip 本机就是127.0.0.1
redis.port=6379
redis.passwd=你的password 与你启动的redis保持一致
redis.maxIdle=2000
redis.maxActive=60000
redis.maxWait=1000
redis.testOnBorrow=true
redis.timeout=100000
defaultCacheExpireTime=60
3.编写自定义RedisCache类,实现mybatis的二级缓存Cache接口
package com.zeal.shiyulin.common;



import org.apache.ibatis.cache.Cache;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.connection.jedis.JedisConnection;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import redis.clients.jedis.exceptions.JedisConnectionException;

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;


/**
*使用第三方内存数据库Redis作为二级缓存
*@author  zeal
*@date 2017/12/8 19:32
*/
public class RedisCache implements Cache {
    private static final Logger logger = LoggerFactory.getLogger(RedisCache.class);

    private static JedisConnectionFactory jedisConnectionFactory;

    private final String id;

    /**
     * The {@code ReadWriteLock}.
     */
    private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    public RedisCache(final String id) {
        if (id == null) {
            throw new IllegalArgumentException("Cache instances require an ID");
        }
        logger.debug("MybatisRedisCache:id=" + id);
        this.id = id;
    }

    @Override
    public void clear()
    {
        JedisConnection connection = null;
        try
        {
            connection = jedisConnectionFactory.getConnection();
            connection.flushDb();
            connection.flushAll();
        }
        catch (JedisConnectionException e)
        {
            e.printStackTrace();
        }
        finally
        {
            if (connection != null) {
                connection.close();
            }
        }
    }

    @Override
    public String getId()
    {
        return this.id;
    }

    @Override
    public Object getObject(Object key)
    {
        Object result = null;
        JedisConnection connection = null;
        try
        {
            connection = jedisConnectionFactory.getConnection();
            RedisSerializer<Object> serializer = new JdkSerializationRedisSerializer();
            result = serializer.deserialize(connection.get(serializer.serialize(key)));
        }
        catch (JedisConnectionException e)
        {
            e.printStackTrace();
        }
        finally
        {
            if (connection != null) {
                connection.close();
            }
        }
        return result;
    }

    @Override
    public ReadWriteLock getReadWriteLock()
    {
        return this.readWriteLock;
    }

    @Override
    public int getSize()
    {
        int result = 0;
        JedisConnection connection = null;
        try
        {
            connection = jedisConnectionFactory.getConnection();
            result = Integer.valueOf(connection.dbSize().toString());
        }
        catch (JedisConnectionException e)
        {
            e.printStackTrace();
        }
        finally
        {
            if (connection != null) {
                connection.close();
            }
        }
        return result;
    }

    @Override
    public void putObject(Object key, Object value)
    {
        JedisConnection connection = null;
        try
        {
            connection = jedisConnectionFactory.getConnection();
            RedisSerializer<Object> serializer = new JdkSerializationRedisSerializer();
            connection.set(serializer.serialize(key), serializer.serialize(value));
        }
        catch (JedisConnectionException e)
        {
            e.printStackTrace();
        }
        finally
        {
            if (connection != null) {
                connection.close();
            }
        }
    }

    @Override
    public Object removeObject(Object key)
    {
        JedisConnection connection = null;
        Object result = null;
        try
        {
            connection = jedisConnectionFactory.getConnection();
            RedisSerializer<Object> serializer = new JdkSerializationRedisSerializer();
            result =connection.expire(serializer.serialize(key), 0);
        }
        catch (JedisConnectionException e)
        {
            e.printStackTrace();
        }
        finally
        {
            if (connection != null) {
                connection.close();
            }
        }
        return result;
    }

    public static void setJedisConnectionFactory(JedisConnectionFactory jedisConnectionFactory) {
        RedisCache.jedisConnectionFactory = jedisConnectionFactory;
    }

}

4.使用中间类RedisCacheTransfer静态注入jedisConnectionFactory

注意: 这里有个小坑,在实现静态注入的时候,如果在spring-context.xml里面设置了延迟实例化default-lazy-init=”ture”的话,需要改为default-lazy-init=”false”,否则有可能会报jedisConnectionFactory空指针。
在静态注入redisCacheTransfer的时候,应该这样配置。

    <bean id="redisCacheTransfer" class="com.zeal.shiyulin.common.RedisCacheTransfer" lazy-init="false">
        <property name="jedisConnectionFactory" ref="jedisConnectionFactory"/>
    </bean>
package com.zeal.shiyulin.common;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;

/**
*静态注入中间类
*@author  zeal
*@date 2017/12/8 19:31
*/
public class RedisCacheTransfer {


    @Autowired
    public  void setJedisConnectionFactory(JedisConnectionFactory jedisConnectionFactory) {
        RedisCache.setJedisConnectionFactory(jedisConnectionFactory);
    }
}

5.配置spring-context.xml,将redis与spring整合
    <!-- 配置redis 单机版 -->
     <!--redis数据源 -->
    <bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig">
        <!-- 最大空闲数 -->
        <property name="maxIdle" value="${redis.maxIdle}" />
        <!-- 最大空闲数 -->
        <property name="maxTotal" value="${redis.maxActive}" />
        <!-- 最大等待时间 -->
        <property name="maxWaitMillis" value="${redis.maxWait}" />
        <!-- 返回连接时,检测连接是否成功 -->
        <property name="testOnBorrow" value="${redis.testOnBorrow}" />

    </bean>

    <!-- Spring-redis连接池管理工厂 -->
    <bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
        <!-- IP地址 -->
        <property name="hostName" value="${redis.host}" />
        <!-- 端口号 -->
        <property name="port" value="${redis.port}" />
        <!-- 登录密码 -->
        <property name="password" value="${redis.passwd}"/>
        <!-- 超时时间 -->
        <property name="timeout" value="${redis.timeout}" />
        <property name="poolConfig" ref="poolConfig" />
    </bean>

    <!-- redis 集群 -->
    <!--  <bean id="redisClient" class="redis.clients.jedis.JedisCluster">
          <constructor-arg name="nodes">
              <bean class="redis.clients.jedis.HostAndPort">
                <constructor-arg name="host" value="192.168.113.115"></constructor-arg>
                <constructor-arg name="port" value="7002"></constructor-arg>
            </bean>
            <bean class="redis.clients.jedis.HostAndPort">
                <constructor-arg name="host" value="192.168.17.37"></constructor-arg>
                <constructor-arg name="port" value="7002"></constructor-arg>
            </bean>
          </constructor-arg>
     </bean>  -->

    <!-- redis模板类,提供了对缓存的增删改查 -->
    <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
        <property name="connectionFactory" ref="jedisConnectionFactory" />
        <!--     如果不配置Serializer,那么存储的时候只能使用String,如果用对象类型存储,那么会提示错误 can't cast to String!!!-->
        <property name="keySerializer">
            <bean class="org.springframework.data.redis.serializer.StringRedisSerializer" />
        </property>
        <property name="valueSerializer">
            <bean class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer" />
        </property>
        <!--开启事务-->
        <property name="enableTransactionSupport" value="true"/>
    </bean>

    <!-- StrRedisTemplate -->
    <bean id="strRedisTemplate" class="org.springframework.data.redis.core.StringRedisTemplate">
        <property name="connectionFactory" ref="jedisConnectionFactory" />
        <property name="keySerializer">
            <bean class="org.springframework.data.redis.serializer.StringRedisSerializer" />
        </property>
        <property name="valueSerializer">
            <bean class="org.springframework.data.redis.serializer.StringRedisSerializer" />
        </property>
        <property name="hashKeySerializer">
            <bean class="org.springframework.data.redis.serializer.StringRedisSerializer" />
        </property>
    </bean>

    <!-- 使用中间类解决RedisCache.jedisConnectionFactory的静态注入,从而使MyBatis实现第三方缓存 -->
    <bean id="redisCacheTransfer" class="com.zeal.shiyulin.common.RedisCacheTransfer" >
        <property name="jedisConnectionFactory" ref="jedisConnectionFactory"/>
    </bean>

    <!-- //End 单机版Redis集成 -->

    <!-- Redis缓存管理对象 -->
    <bean id="redisCacheManager" class="org.springframework.data.redis.cache.RedisCacheManager">
        <constructor-arg index="0" ref="redisTemplate" />
    </bean>

然后启动spring的cache,并且引入cache命名空间头部

<!--启用缓存注解-->
<cache:annotation-driven cache-manager="redisCacheManager"/>
xmlns:cache="http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache-4.0.xsd

6.配置mybatis.config,开启mybatis二级缓存,并且在初始化sqlSessionFactory将其配置引入

mybatis-config.xml关键配置如下。省去了一部分配置

    <!-- 全局参数 -->
    <settings>
        <!-- 使全局的映射器启用或禁用缓存。 -->
        <setting name="cacheEnabled" value="true"/>

        <!-- 全局启用或禁用延迟加载。当禁用时,所有关联对象都会即时加载。 -->
        <setting name="lazyLoadingEnabled" value="true"/>

        <!-- 当启用时,有延迟加载属性的对象在被调用时将会完全加载任意属性。否则,每种属性将会按需要加载。 -->
        <setting name="aggressiveLazyLoading" value="true"/>
    <settings>

spring-context.xml关于数据源会话工厂配置如下

    <!-- 数据库配置数据源 MyBatis begin -->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <property name="typeAliasesPackage" value="com.zeal.shiyulin"/>
        <property name="mapperLocations" value="classpath:/mappings/**/*.xml"/>
        <property name="configLocation" value="classpath:/mybatis-config.xml"></property>
    </bean>
7.在mapper.xml文件中启用二级缓存
<mapper namespace="com.zeal.shiyulin.modules.article.dao.ArticleDao">
    <cache type="com.zeal.shiyulin.common.RedisCache"/>
        <!-- 根据ID来查找一条Article -->
    <select id="findById" parameterType="java.lang.String"
            resultType="com.zeal.shiyulin.modules.article.entity.ArticleEntity">
        select * from article
        where id = #{id}

    </select>
</mapper>

启动项目,就可以实现redis对mybatis的二级缓存支持。

二、使用spring注解形式

**思路:* 实现的思路是以spring自带的cacheManager来管理redis,实现注解(acheable、@CachePut、@CacheEvict)形式使用,这样子可以更加灵活缓存需要缓存的数据

1.所需要的依赖
        <!-- redis客户端  jedis -->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>2.8.0</version>
        </dependency>
        <!-- spring-redis实现 -->
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-redis</artifactId>
            <version>1.6.2.RELEASE</version>
        </dependency>
2.配置redis.properties
#redis settings
redis.host= 你的host ip 本机就是127.0.0.1
redis.port=6379
redis.passwd=你的password 与你启动的redis保持一致
redis.maxIdle=2000
redis.maxActive=60000
redis.maxWait=1000
redis.testOnBorrow=true
redis.timeout=100000
defaultCacheExpireTime=60
3.自定义ComponentRedisCache类实现spring的cache接口
package com.zeal.shiyulin.common;/**
 * Created by Zeal on 2017/12/10.
 */


import org.springframework.cache.Cache;
import org.springframework.cache.support.SimpleValueWrapper;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;

import java.io.*;

/**
 * 注解式的redisCache
 *
 * @auther Zeal
 * @create 2017/12/10
 **/
public class ComponentRedisCache implements Cache {
    private RedisTemplate<String, Object> redisTemplate;
    private String name;
    public RedisTemplate<String, Object> getRedisTemplate() {
        return redisTemplate;
    }

    public void setRedisTemplate(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String getName() {
        // TODO Auto-generated method stub
        return this.name;
    }

    @Override
    public Object getNativeCache() {
        // TODO Auto-generated method stub
        return this.redisTemplate;
    }

    @Override
    public org.springframework.cache.Cache.ValueWrapper get(Object key) {
        // TODO Auto-generated method stub
        System.out.println("get key");
        final String keyf =  key.toString();
        Object object = null;
        object = redisTemplate.execute(new RedisCallback<Object>() {
            public Object doInRedis(RedisConnection connection)
                    throws DataAccessException {
                byte[] key = keyf.getBytes();
                byte[] value = connection.get(key);
                if (value == null) {
                    return null;
                }
                return toObject(value);
            }
        });
        return (object != null ? new SimpleValueWrapper(object) : null);
    }

    @Override
    public void put(Object key, Object value) {
        // TODO Auto-generated method stub
        System.out.println("put key");
        final String keyf = key.toString();
        final Object valuef = value;
        final long liveTime = 86400;
        redisTemplate.execute(new RedisCallback<Long>() {
            public Long doInRedis(RedisConnection connection)
                    throws DataAccessException {
                byte[] keyb = keyf.getBytes();
                byte[] valueb = toByteArray(valuef);
                connection.set(keyb, valueb);
                if (liveTime > 0) {
                    connection.expire(keyb, liveTime);
                }
                return 1L;
            }
        });
    }

    private byte[] toByteArray(Object obj) {
        byte[] bytes = null;
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        try {
            ObjectOutputStream oos = new ObjectOutputStream(bos);
            oos.writeObject(obj);
            oos.flush();
            bytes = bos.toByteArray();
            oos.close();
            bos.close();
        }catch (IOException ex) {
            ex.printStackTrace();
        }
        return bytes;
    }

    private Object toObject(byte[] bytes) {
        Object obj = null;
        try {
            ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
            ObjectInputStream ois = new ObjectInputStream(bis);
            obj = ois.readObject();
            ois.close();
            bis.close();
        } catch (IOException ex) {
            ex.printStackTrace();
        } catch (ClassNotFoundException ex) {
            ex.printStackTrace();
        }
        return obj;
    }

    @Override
    public void evict(Object key) {
        // TODO Auto-generated method stub
        System.out.println("del key");
        final String keyf = key.toString();
        redisTemplate.execute(new RedisCallback<Long>() {
            public Long doInRedis(RedisConnection connection)
                    throws DataAccessException {
                return connection.del(keyf.getBytes());
            }
        });
    }

    @Override
    public void clear() {
        // TODO Auto-generated method stub
        System.out.println("clear key");
        redisTemplate.execute(new RedisCallback<String>() {
            public String doInRedis(RedisConnection connection)
                    throws DataAccessException {
                connection.flushDb();
                return "ok";
            }
        });
    }

    @Override
    public <T> T get(Object key, Class<T> type) {
        // TODO Auto-generated method stub
        return null;
    }

    @Override
    public ValueWrapper putIfAbsent(Object key, Object value) {
        // TODO Auto-generated method stub
        return null;
    }
}

4.配置spring-context.xml,将redis与spring整合
<!-- 配置redis 单机版 -->
     <!--redis数据源 -->
    <bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig">
        <!-- 最大空闲数 -->
        <property name="maxIdle" value="${redis.maxIdle}" />
        <!-- 最大空闲数 -->
        <property name="maxTotal" value="${redis.maxActive}" />
        <!-- 最大等待时间 -->
        <property name="maxWaitMillis" value="${redis.maxWait}" />
        <!-- 返回连接时,检测连接是否成功 -->
        <property name="testOnBorrow" value="${redis.testOnBorrow}" />

    </bean>

    <!-- Spring-redis连接池管理工厂 -->
    <bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
        <!-- IP地址 -->
        <property name="hostName" value="${redis.host}" />
        <!-- 端口号 -->
        <property name="port" value="${redis.port}" />
        <!-- 登录密码 -->
        <property name="password" value="${redis.passwd}"/>
        <!-- 超时时间 -->
        <property name="timeout" value="${redis.timeout}" />
        <property name="poolConfig" ref="poolConfig" />
    </bean>

    <!-- redis 集群 -->
    <!--  <bean id="redisClient" class="redis.clients.jedis.JedisCluster">
          <constructor-arg name="nodes">
              <bean class="redis.clients.jedis.HostAndPort">
                <constructor-arg name="host" value="192.168.113.115"></constructor-arg>
                <constructor-arg name="port" value="7002"></constructor-arg>
            </bean>
            <bean class="redis.clients.jedis.HostAndPort">
                <constructor-arg name="host" value="192.168.17.37"></constructor-arg>
                <constructor-arg name="port" value="7002"></constructor-arg>
            </bean>
          </constructor-arg>
     </bean>  -->

    <!-- redis模板类,提供了对缓存的增删改查 -->
    <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
        <property name="connectionFactory" ref="jedisConnectionFactory" />
        <!--     如果不配置Serializer,那么存储的时候只能使用String,如果用对象类型存储,那么会提示错误 can't cast to String!!!-->
        <property name="keySerializer">
            <bean class="org.springframework.data.redis.serializer.StringRedisSerializer" />
        </property>
        <property name="valueSerializer">
            <bean class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer" />
        </property>
        <!--开启事务-->
        <property name="enableTransactionSupport" value="true"/>
    </bean>

    <!-- StrRedisTemplate -->
    <bean id="strRedisTemplate" class="org.springframework.data.redis.core.StringRedisTemplate">
        <property name="connectionFactory" ref="jedisConnectionFactory" />
        <property name="keySerializer">
            <bean class="org.springframework.data.redis.serializer.StringRedisSerializer" />
        </property>
        <property name="valueSerializer">
            <bean class="org.springframework.data.redis.serializer.StringRedisSerializer" />
        </property>
        <property name="hashKeySerializer">
            <bean class="org.springframework.data.redis.serializer.StringRedisSerializer" />
        </property>
    </bean>


    <!-- //End 单机版Redis集成 -->



    <!--spring自己的缓存管理器,用于redis以spring注解的方式来缓存,这里定义了缓存位置名称 ,即注解中的value-->
    <bean id="springCacheManager" class="org.springframework.cache.support.SimpleCacheManager">
        <property name="caches">
            <set>
                <!-- 这里可以配置多个redis -->
                <!-- <bean class="com.zeal.shiyulin.common.ComponentRedisCache">
                     <property name="redisTemplate" ref="redisTemplate" />
                     <property name="name" value="default"/>
                </bean> -->
                <bean class="com.zeal.shiyulin.common.ComponentRedisCache">
                    <property name="redisTemplate" ref="redisTemplate" />
                    <property name="name" value="common"/>
                    <!-- common名称要在类或方法的注解中使用 -->
                </bean>
            </set>
        </property>
    </bean>

然后启动spring的cache,并且引入cache命名空间头部

<!--启用缓存注解-->
<cache:annotation-driven cache-manager="springCacheManager"/>
xmlns:cache="http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache-4.0.xsd

5.配置mybatis.config,开启mybatis二级缓存,并且在初始化sqlSessionFactory将其配置引入

mybatis-config.xml关键配置如下。省去了一部分配置

    <!-- 全局参数 -->
    <settings>
        <!-- 使全局的映射器启用或禁用缓存。 -->
        <setting name="cacheEnabled" value="true"/>

        <!-- 全局启用或禁用延迟加载。当禁用时,所有关联对象都会即时加载。 -->
        <setting name="lazyLoadingEnabled" value="true"/>

        <!-- 当启用时,有延迟加载属性的对象在被调用时将会完全加载任意属性。否则,每种属性将会按需要加载。 -->
        <setting name="aggressiveLazyLoading" value="true"/>
    <settings>

spring-context.xml关于数据源会话工厂配置如下

    <!-- 数据库配置数据源 MyBatis begin -->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <property name="typeAliasesPackage" value="com.zeal.shiyulin"/>
        <property name="mapperLocations" value="classpath:/mappings/**/*.xml"/>
        <property name="configLocation" value="classpath:/mybatis-config.xml"></property>
    </bean>
6.在service里面调用方法,注解cache
    /**
     * 通过ID查找
     * @param id
     * @return
     */
    //Ehcache的spring缓存注解,在common中缓存id为id的对象
    @Cacheable(value = "common,key="#id")
    public Result findById(String id){
        return new Result(dao.findById(id));
    }

总结:

在对框架进行缓存的时候,两种方式都是可行的,基于mapper.xml全局配置的方式的优点是可以自动管理,redis会自动检测object的内容有没有更新,每次在执行dao方法的时候会先检查缓存有没有更改过,如果有的话则会从数据库中查询,而保证不会得到过时的数据,这一点不需要代码来管理非常方便。
而基于注解的方式则是更加灵活,你可以将一些常用的查询方法缓存起来,你也可以不缓存一些不需要的数据,但是缺点是每次执行update或者delete方法的时候需要进行处理,将最新数据更新到缓存中去,不然会得到过时的不正确的数据。
==当然,除了用redis作为缓存的话,如果你的系统是中小型,不涉及到大型分布式的话,使用ehcache作为缓存,也是一个很不错的选择,因为ehcache是直接缓存在内存中的,读取速度会更快,而且配置很简单,很容易上手,并且比较轻量级,同时也支持mapper配置和注解两种实现方式,请关注我下一篇对ehcache的整合==


zeal

一个喜欢安静,追求技术的全栈程序猿。

5 个评论

行走的土豆 · 2018年5月27日 - 下午9:21

写的很好,刚好最近在整合redis,谢谢博主啊。

    至尊宝ss · 2018年5月28日 - 上午11:22

    层主有demo吗?一起学习啊

超人不会飞 · 2018年5月27日 - 下午9:26

意外发现了一个风水宝地,哈哈

Zeal · 2018年6月1日 - 下午4:04

我是第一个注册的吗?

Zeal · 2018年6月1日 - 下午4:56

头像怎么不显示啊

发表评论

电子邮件地址不会被公开。 必填项已用*标注

我不是机器人*