(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

开启二级缓存

1
2
3
4
5
#Mybatis
mybatis.mapper-locations=classpath:com/sunnada/hurd/*/dao/*.xml
mybatis.type-aliases-package=com.sunnada.hurd
#开启MyBatis的二级缓存
mybatis.configuration.cache-enabled=true

配置redis

1
2
3
4
5
6
7
8
9
#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依赖

1
2
3
4
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

RedisConfig

这里Redis配置类重写了Redis序列化的方式,改用Json的数据结构传输数据。

配置RedisTemplate并定义Serializer方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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;
    }
 
 
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
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");
        }
    }
}

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接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
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;
    }
}

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映射配置文件,只需要引入刚刚配置好的自定义缓存类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<?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>

<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开启缓存。

1
2
3
<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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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;
    }
 
}

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 获取列表 (第一次)

(springboot)基于Redis实现Mybatis二级缓存(自定义缓存)

(第二次)

(springboot)基于Redis实现Mybatis二级缓存(自定义缓存)

速度明显提升。

发表评论

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

关注我们