springboot mybatis redis 二級緩存


前言

什么是mybatis二級緩存?

二級緩存是多個sqlsession共享的,其作用域是mapper的同一個namespace。

即,在不同的sqlsession中,相同的namespace下,相同的sql語句,並且sql模板中參數也相同的,會命中緩存。

第一次執行完畢會將數據庫中查詢的數據寫到緩存,第二次會從緩存中獲取數據將不再從數據庫查詢,從而提高查詢效率。

Mybatis默認沒有開啟二級緩存,需要在全局配置(mybatis-config.xml)中開啟二級緩存。

 

本文講述的是使用Redis作為緩存,與springboot、mybatis進行集成的方法。

 

1、pom依賴

使用springboot redis集成包,方便redis的訪問。redis客戶端選用Jedis。

另外讀寫kv緩存會進行序列化,所以引入了一個序列化包。

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>2.8.0</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.19</version>
        </dependency>

 

依賴搞定之后,下一步先調通Redis客戶端。

 

2、Redis訪問使用的Bean

增加Configuration,配置jedisConnectionFactory bean,留待后面使用。

一般來講,也會生成了redisTemplate bean,但是在接下來的場景沒有使用到。

@Configuration
public class RedisConfig {

    @Value("${spring.redis.host}")
    private String host;
    // 篇幅受限,省略了

    @Bean
    public JedisPoolConfig getRedisConfig(){
        JedisPoolConfig config = new JedisPoolConfig();
        config.setMaxIdle(maxIdle);
        config.setMaxTotal(maxTotal);
        config.setMaxWaitMillis(maxWaitMillis);
        config.setMinIdle(minIdle);
        return config;
    }

    @Bean(name = "jedisConnectionFactory")
    public JedisConnectionFactory getConnectionFactory(){
        JedisConnectionFactory factory = new JedisConnectionFactory();
        JedisPoolConfig config = getRedisConfig();
        factory.setPoolConfig(config);
        factory.setHostName(host);
        factory.setPort(port);
        factory.setDatabase(database);
        factory.setPassword(password);
        factory.setTimeout(timeout);
        return factory;
    }

    @Bean(name = "redisTemplate")
    public RedisTemplate<?, ?> getRedisTemplate(){
        RedisTemplate<?,?> template = new StringRedisTemplate(getConnectionFactory());
        return template;
    }
} 

這里使用@Value讀入了redis相關配置,有更簡單的配置讀取方式(@ConfigurationProperties(prefix=...)),可以嘗試使用。

Redis相關配置如下

#redis
spring.redis.host=10.93.84.53
spring.redis.port=6379
spring.redis.password=bigdata123
spring.redis.database=15
spring.redis.timeout=0

spring.redis.pool.maxTotal=8
spring.redis.pool.maxWaitMillis=1000
spring.redis.pool.maxIdle=8
spring.redis.pool.minIdle=0

Redis客戶端的配置含義,這里不再講解了。pool相關的一般都和性能有關,需要根據並發量權衡句柄、內存等資源進行設置。

 

Redis客戶端設置好了,我們開始配置Redis作為Mybatis的緩存。

 

3、Mybatis Cache

這一步是最為關鍵的一步。實現方式是實現Mybatis的一個接口org.apache.ibatis.cache.Cache。

這個接口設計了寫緩存,讀緩存,銷毀緩存的方式,和訪問控制讀寫鎖。

我們實現實現Cache接口的類是MybatisRedisCache。

MybatisRedisCache.java

public class MybatisRedisCache implements Cache {

    private static JedisConnectionFactory jedisConnectionFactory;

    private final String id;

    private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

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

    @Override
    public void clear() {
        RedisConnection 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;
        RedisConnection 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;
        RedisConnection 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) {
        RedisConnection 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) {
        RedisConnection 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) {
        MybatisRedisCache.jedisConnectionFactory = jedisConnectionFactory;
    }

}

注意:

可以看到,這個類並不是由Spring虛擬機管理的類,但是,其中有一個靜態屬性jedisConnectionFactory需要注入一個Spring bean,也就是在RedisConfig中生成的bean。

在一個普通類中使用Spring虛擬機管理的Bean,一般使用Springboot自省的SpringContextAware。

 

這里使用了另一種方式,靜態注入的方式。這個方式是通過RedisCacheTransfer來實現的。

 

4、靜態注入

RedisCacheTransfer.java

@Component
public class RedisCacheTransfer {

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

}

可以看到RedisCacheTransfer是一個springboot bean,在容器創建之初進行初始化的時候,會注入jedisConnectionFactory bean給setJedisConnectionFactory方法的傳參。

而setJedisConnectionFactory通過調用靜態方法設置了類MybatisRedisCache的靜態屬性jedisConnectionFactory。

這樣就把spring容器管理的jedisConnectionFactory注入到了靜態域。

 

到這里,代碼基本已經搞定,下面是一些配置。主要有(1)全局開關;(2)namespace作用域開關;(3)Model實例序列化。

 

5、Mybatis二級緩存的全局開關

前面提到過,默認二級緩存沒有打開,需要設置為true。這是全局二級緩存的開關。

Mybatis的全局配置。

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">

<configuration>
    <!-- 全局參數 -->
    <settings>
        <!-- 使全局的映射器啟用或禁用緩存。 -->
        <setting name="cacheEnabled" value="true"/>
    </settings>

</configuration>

全局配置的加載在dataSource中可以是這樣的。

bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mybatis-mapper/*.xml"));

指定了mapper.xml的存放路徑,在mybatis-mapper路徑下,所有后綴是.xml的都會讀入。

bean.setConfigLocation(new ClassPathResource("mybatis-config.xml"));

指定了mybatis-config.xml的存放路徑,直接放在Resource目錄下即可。

@Bean(name = "moonlightSqlSessionFactory")
    @Primary
    public SqlSessionFactory moonlightSqlSessionFactory(@Qualifier("moonlightData") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dataSource);
        bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mybatis-mapper/*.xml"));
        bean.setConfigLocation(new ClassPathResource("mybatis-config.xml"));
        return bean.getObject();
    }

 

6、配置mapper作用域namespace

前面提到過,二級緩存的作用域是mapper的namespace,所以這個配置需要到mapper中去寫。

<mapper namespace="com.kangaroo.studio.moonlight.dao.mapper.MoonlightMapper">
  <cache type="com.kangaroo.studio.moonlight.dao.cache.MybatisRedisCache"/>
  <resultMap id="geoFenceList" type="com.kangaroo.studio.moonlight.dao.model.GeoFence">
    <constructor>
      <idArg column="id" javaType="java.lang.Integer" jdbcType="INTEGER" />
      <arg column="name" javaType="java.lang.String" jdbcType="VARCHAR" />
      <arg column="type" javaType="java.lang.Integer" jdbcType="INTEGER" />
      <arg column="group" javaType="java.lang.String" jdbcType="VARCHAR" />
      <arg column="geo" javaType="java.lang.String" jdbcType="VARCHAR" />
      <arg column="createTime" javaType="java.lang.String" jdbcType="VARCHAR" />
      <arg column="updateTime" javaType="java.lang.String" jdbcType="VARCHAR" />
    </constructor>
  </resultMap>

<select id="queryGeoFence" parameterType="com.kangaroo.studio.moonlight.dao.model.GeoFenceQueryParam" resultMap="geoFenceList">
    select <include refid="base_column"/> from geoFence where 1=1
    <if test="type != null">
      and type = #{type}
    </if>
    <if test="name != null">
      and name like concat('%', #{name},'%')
    </if>
    <if test="group != null">
      and `group` like concat('%', #{group},'%')
    </if>
    <if test="startTime != null">
      and createTime &gt;= #{startTime}
    </if>
    <if test="endTime != null">
      and createTime &lt;= #{endTime}
    </if>
  </select>
</mapper>

注意:

namespace下的cache標簽就是加載緩存的配置,緩存使用的正式我們剛才實現的MybatisRedisCache。

<cache type="com.kangaroo.studio.moonlight.dao.cache.MybatisRedisCache"/>

 這里只實現了一個查詢queryGeoFence,你可以在select標簽中,開啟或者關閉這個sql的緩存。使用屬性值useCache=true/false。

 

7、Mapper和Model

讀寫緩存Model需要序列化:只需要類聲明的時候實現Seriaziable接口就好了。

public class GeoFence implements Serializable {
    // setter和getter省略  
}
public class GeoFenceParam implements Serializable {
    // setter和getter省略  
}

 

mapper就還是以前的寫法,使用mapper.xml的方式這里只需要定義出抽象函數即可。

@Mapper
public interface MoonlightMapper {
    List<GeoFence> queryGeoFence(GeoFenceQueryParam geoFenceQueryParam);
}

 

 

到這里,所有的代碼和配置都完成了,下面測試一下。

 

8、測試一下

Controller中實現一個這樣的接口POST。

@RequestMapping(value = "/fence/query", method = RequestMethod.POST)
    @ResponseBody
    public ResponseEntity<Response> queryFence(@RequestBody GeoFenceQueryParam geoFenceQueryParam) {
        try {
            Integer pageNum = geoFenceQueryParam.getPageNum()!=null?geoFenceQueryParam.getPageNum():1;
            Integer pageSize = geoFenceQueryParam.getPageSize()!=null?geoFenceQueryParam.getPageSize():10;
            PageHelper.startPage(pageNum, pageSize);
            List<GeoFence> list = moonlightMapper.queryGeoFence(geoFenceQueryParam);
            return new ResponseEntity<>(
                    new Response(ResultCode.SUCCESS, "查詢geoFence成功", list),
                    HttpStatus.OK);
        } catch (Exception e) {
            logger.error("查詢geoFence失敗", e);
            return new ResponseEntity<>(
                    new Response(ResultCode.EXCEPTION, "查詢geoFence失敗", null),
                    HttpStatus.INTERNAL_SERVER_ERROR);
        }

 使用curl發送請求,注意

1)-H - Content-type:application/json方式

2)-d - 后面是json格式的參數包體

curl -H "Content-Type:application/json" -XPOST http://。。。/moonlight/fence/query -d '{
    "name" : "test",
    "group": "test", "type": 1, "startTime":"2017-12-06 00:00:00", "endTime":"2017-12-06 16:00:00", "pageNum": 1, "pageSize": 8 }'

請求了三次,日志打印如下,

可以看到,只有第一次執行了sql模板查詢,后面都是命中了緩存。

在我們的測試環境中由於數據量比較小,緩存對查詢速度的優化並不明顯。這里就不過多說明了。

最后上一篇打臉文。給你參考http://blog.csdn.net/isea533/article/details/44566257 

 

 完畢。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM