1. 程式人生 > >(springboot)基於Redis實現Mybatis二級快取(自定義快取)

(springboot)基於Redis實現Mybatis二級快取(自定義快取)

Springboot + Mybatis + Redis

Mybatis的二級快取是多個SqlSession共享的,作用於是mapper配置檔案中同一個namespace,不同的SqlSession兩次執行相同namespace下的sql語句且引數如果也一樣則最終執行的sql語句是相同的。每次查詢都會先看看快取中是否有對應查詢結果,如果有就從快取拿,如果沒有就執行sql語句從資料庫中讀取,從而提高查詢效率。Mybatis預設開啟的是一級快取,所以二級快取需要自己手動開啟。

ps: 本專案是基於springboot + mybatis 環境下配置Redis

環境

  • 開發環境:Window 10
  • IDE: Intellij 2017.2
  • JDK: 1.8
  • Redis:3.2.100
  • Oracle:12.1.0.2.0

application.properties

開啟二級快取

#Mybatis
mybatis.mapper-locations=classpath:com/sunnada/hurd/*/dao/*.xml
mybatis.type-aliases-package=com.sunnada.hurd
#開啟MyBatis的二級快取
mybatis.configuration.cache-enabled=true

配置redis

#redis
#database name
spring.redis.database=0
#server host
spring.redis.host=192.168.168.9
#server password
spring.redis.password=
#connection port
spring.redis.port=6379

pom.xml

新增redis依賴

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

RedisConfig

這裡Redis配置類重寫了Redis序列化的方式,改用Json的資料結構傳輸資料。

配置RedisTemplate並定義Serializer方式。

package com.sunnada.hurd.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * @program: HurdProject
 * @description: Redis配置類
 * @author: linyh
 * @create: 2018-09-10 17:17
 **/
@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);

        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);

        // 設定值(value)的序列化採用Jackson2JsonRedisSerializer。
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        // 設定鍵(key)的序列化採用StringRedisSerializer。
        redisTemplate.setKeySerializer(new StringRedisSerializer());

        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }


}

SpringContextHolder

元件,實現了Spring的ApplicationContextAware來獲取ApplicationContext,從中獲取容器的bean

package com.sunnada.hurd.cache;

import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

/**
 * @description: 以靜態變數儲存Spring ApplicationContext, 可在任何程式碼任何地方任何時候中取出ApplicaitonContext
 * @author: linyh
 * @create: 2018-09-10 17:25
 **/
@Component
public class SpringContextHolder implements ApplicationContextAware{

    private static ApplicationContext applicationContext;

    /**
     * 實現ApplicationContextAware介面的context注入函式, 將其存入靜態變數.
     */
    public void setApplicationContext(ApplicationContext applicationContext) {
        SpringContextHolder.applicationContext = applicationContext; // NOSONAR
    }

    /**
     * 取得儲存在靜態變數中的ApplicationContext.
     */
    public static ApplicationContext getApplicationContext() {
        checkApplicationContext();
        return applicationContext;
    }

    /**
     * 從靜態變數ApplicationContext中取得Bean, 自動轉型為所賦值物件的型別.
     */
    @SuppressWarnings("unchecked")
    public static <T> T getBean(String name) {
        checkApplicationContext();
        return (T) applicationContext.getBean(name);
    }

    /**
     * 從靜態變數ApplicationContext中取得Bean, 自動轉型為所賦值物件的型別.
     */
    @SuppressWarnings("unchecked")
    public static <T> T getBean(Class<T> clazz) {
        checkApplicationContext();
        return (T) applicationContext.getBeansOfType(clazz);
    }

    /**
     * 清除applicationContext靜態變數.
     */
    public static void cleanApplicationContext() {
        applicationContext = null;
    }

    private static void checkApplicationContext() {
        if (applicationContext == null) {
            throw new IllegalStateException("applicaitonContext未注入,請在applicationContext.xml中定義SpringContextHolder");
        }
    }
}

MybatisRedisCache

Mybatis二級快取預設使用的是其他的快取,這裡我們需要整合Redis就需要自己自定義寫一個快取類去實現二級快取。

自定義快取需要實現Mybatis的Cache介面。

package com.sunnada.hurd.cache;

import org.apache.ibatis.cache.Cache;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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 org.springframework.util.CollectionUtils;

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

/**
 * @description: 使用Redis實現Mybatis二級快取,實現Cache介面
 * @author: linyh
 * @create: 2018-09-10 17:21
 **/
public class MybatisRedisCache implements Cache {

    //private static final Logger logger = LoggerFactory.getLogger(MybatisRedisCache.class);

    // 讀寫鎖
    private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(true);

    private RedisTemplate<String, Object> redisTemplate = SpringContextHolder.getBean("redisTemplate");

    private String id;

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

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

    @Override
    public void putObject(Object key, Object value) {
        if (value != null) {
            // 向Redis中新增資料,有效時間是2天
            redisTemplate.opsForValue().set(key.toString(), value, 2, TimeUnit.DAYS);
        }
    }

    @Override
    public Object getObject(Object key) {
        try {
            if (key != null) {
                Object obj = redisTemplate.opsForValue().get(key.toString());
                return obj;
            }
        } catch (Exception e) {
            //logger.error("redis ");
        }
        return null;
    }

    @Override
    public Object removeObject(Object key) {
        try {
            if (key != null) {
                redisTemplate.delete(key.toString());
            }
        } catch (Exception e) {
        }
        return null;
    }

    @Override
    public void clear() {
        //logger.debug("清空快取");
        try {
            Set<String> keys = redisTemplate.keys("*:" + this.id + "*");
            if (!CollectionUtils.isEmpty(keys)) {
                redisTemplate.delete(keys);
            }
        } catch (Exception e) {
        }
    }

    @Override
    public int getSize() {
        Long size = (Long) redisTemplate.execute(new RedisCallback<Long>() {
            @Override
            public Long doInRedis(RedisConnection connection) throws DataAccessException {
                return connection.dbSize();
            }
        });
        return size.intValue();
    }

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

Mapper.xml

mapper對映配置檔案,只需要引入剛剛配置好的自定義快取類。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.sunnada.hurd.dictionary.dao.CertificationTypeMapper">
    <!--<resultMap id="DemoResultMap" type="com.sunnada.hurd.demo.pojo.Demo">
        <id column="id" jdbcType="INT" property="id" />
        <result column="name" jdbcType="VARCHAR" property="name" />
    </resultMap>-->

    <cache type="com.sunnada.hurd.cache.MybatisRedisCache">
        <property name="eviction" value="LRU" />
        <property name="flushInterval" value="6000000" />
        <property name="size" value="1024" />
        <property name="readOnly" value="false" />
    </cache>

    <insert id="insert" parameterType="CertificationType" >
        insert into DIS_CERTIFICATION_TYPE ( ID,NAME,CODE,PARENT_ID,SORT,STATUS,CREATE_TIME,MODIFY_TIME,OLD_SYSTEM_ID )
        values (${id},#{name},#{code},#{parentID},#{sort},#{status},#{createTime},#{modifiedTime},#{oldSystemID})
        <selectKey keyProperty="id" order="BEFORE" resultType="int">
            SELECT DIS_CERTIFICATION_TYPE_ID_SEQ.NEXTVAL FROM dual
        </selectKey>
    </insert>

    <delete id="delete" parameterType="java.lang.Integer" >
        delete from DIS_CERTIFICATION_TYPE where ID= #{id}
    </delete>

    <select id="get" parameterType="_int" resultType="CertificationType">
        select * from DIS_CERTIFICATION_TYPE where ID= #{id} and STATUS = 1
    </select>

    <update id="update" parameterType="CertificationType" >
        update DIS_CERTIFICATION_TYPE
        <set>
            <if test="name != null and name.length() > 0">NAME=#{name},</if>
            <if test="code != null and code.length() > 0">CODE=#{code},</if>
            <if test="sort != 0">SORT=#{sort},</if>
            <if test="createTime != null">CREATE_TIME=#{createTime},</if>
            <if test="modifiedTime != null">MODIFY_TIME=#{modifiedTime},</if>
            STATUS=#{status}
        </set>

        where ID=#{id}
    </update>

    <select id="list" parameterType="CertificationType" resultType="CertificationType">
        select * from DIS_CERTIFICATION_TYPE
        <where>
            <if test="name != null and name.length() > 0">
                <bind name="likename" value="'%'+ name +'%'"></bind>
                and NAME like #{likename}
            </if>
            and STATUS = 1
        </where>
    </select>
</mapper>

cache標籤內屬性:

eviction:定義快取移除機制(演算法),預設為LRU(最近最少使用),它會清除最少使用的資料,還有一種FIFO(先進先出),它會清除最先進來的資料。

flushInterval:定義快取重新整理週期,單位為毫秒。

size:標識快取cache中容納的最大元素,預設為1024。

readOnly:預設為false,可配置為true快取只讀。

(雖然我的配置大部分都為預設值,但個人觀點寫出來的話看上去會更清楚一點,所以都寫上吧)

對於有不需要用到二級快取的語句可以在標籤內寫userCache="false",預設為true開啟快取。

<select id="get" parameterType="_int" resultType="CertificationType" useCache="false">
        select * from DIS_CERTIFICATION_TYPE where ID= #{id} and STATUS = 1
</select>

(select 預設useCache為true:使用快取,flushCache為false:不清空快取)

(insert、update、delete 預設flushCache為true:清空快取)

其他的Mapper介面,Service類都照常編寫即可

實體類

實體類需要實現Serializable

package com.sunnada.hurd.dictionary.entity;

import java.io.Serializable;
import java.util.Date;

/**
 * @Author:linyh
 * @Date: 2018/9/6 14:34
 * @Modified By:
 */
public class CertificationType implements Serializable{

    private static final long serialVersionUID = 1L;

    private int id;
    private String name;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

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

}

測試

這裡使用Postman進行測試。

訪問url 獲取列表 (第一次)

(第二次)

速度明顯提升。