(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 獲取列表 (第一次)
(第二次)
速度明顯提升。