# MyBatis教程 - 14 使用Redis作为二级缓存

使用 MyBatis 自身的二级缓存,是基于 JVM 内存的,一旦应用重启或 JVM 崩溃,缓存数据会丢失。而且仅限于单个应用实例的 JVM 内存,无法在多台服务器之间共享缓存数据,适合单体架构。

而 Redis 作为二级缓存相对于 MyBatis 自身的二级缓存,具有分布式支持、持久化、扩展性、性能优化和灵活管理等诸多优势,尤其适用于分布式、高并发、数据量大、需要持久化的场景。


这里我就在 SpringBoot 项目中来集成了,这样集成 MyBatis 和 Redis 比较方便。

SpringBoot 集成 MyBatis,参考 SpringBoot教程 (opens new window) 中的 SpringBoot集成MyBatis

下面的操作,是基于上面的操作继续完成的,假设你完成了上面的集成步骤。

# 14.1 集成Redis

在项目的 pom.xml 添加 Redis 依赖:

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

<!-- redis需要用到的连接池 -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>
1
2
3
4
5
6
7
8
9
10
11

然后 右键 -> Maven -> Reload project

在项目 application.yaml 中添加 redis 配置:

# 数据源配置
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/foooor_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT
    username: root
    password: 123456
    hikari:
      minimum-idle: 5  # 最小空闲连接数
      maximum-pool-size: 10  # 最大活跃连接数
      idle-timeout: 30000  # 空闲连接生命周期,以毫秒为单位
      pool-name: HikariCP  # 连接池名称,主要用于记录日志和JMX管理,默认为生成的
      max-lifetime: 1800000  # 连接在连接池中允许存在的最长时间,默认为30分钟,以毫秒为单位
      connection-timeout: 30000  # 连接超时时间,以毫秒为单位
  
  #-----redis 配置
  data:
    redis:
      host: localhost
      port: 6379
      password: 123456
      database: 0  # 指定使用哪个数据库
      lettuce:
        pool:  # 连接池配置
          # 最大连接数
          max-active: 10
          # 最大阻塞等待时间
          max-wait: 3000
          # 最大空闲连接
          max-idle: 5
          # 最小空闲连接
          min-idle: 2
          enabled: true


# mybaits配置
mybatis:
  # MyBatis Mapper所对应的XML文件位置
  mapper-locations: classpath*:/mapper/*Mapper.xml
  # 类型别名配置
  type-aliases-package: com.foooor.hellomybatis.pojo
  configuration:
    # 开启二级缓存
    cache-enabled: true
    # 开启驼峰命名
    map-underscore-to-camel-case: true
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

MyBatis 开启二级缓存。

# 14.2 配置日志

下面配置一下日志,方便查看 MyBatis 日志,查看执行的 SQL 和缓存命中率。

resources 目录下创建 logback-spring.xml ,内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!-- 定义一个控制台的日志输出 -->
    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>

    <!-- 控制台输出,通常用于开发环境 -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <!-- 设置根日志记录器,如果没有激活任何特定Profile,则使用默认配置 -->
    <root level="info">
        <appender-ref ref="CONSOLE"/>
    </root>

    <!-- 设置 MyBatis 日志级别为 INFO -->
    <logger name="org.mybatis" level="DEBUG" additivity="false">
        <appender-ref ref="CONSOLE"/>
    </logger>

    <logger name="org.apache.ibatis" level="DEBUG" additivity="false">
        <appender-ref ref="CONSOLE"/>
    </logger>

    <!-- 设置特定包的日志级别 -->
    <logger name="com.foooor" level="DEBUG" additivity="false">
        <appender-ref ref="CONSOLE" />
    </logger>

</configuration>
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

上面我只是配置了控制台日志,没有配置输入到文件,如果想查看完整的日志配置,可以查看 SpringBoot教程 (opens new window) 中的 日志配置 (opens new window)

# 14.3 新建缓存类

新建缓存类,实现 org.apache.ibatis.cache.Cache 接口,用于对数据进行序列化和反序列化,并保存到 Redis 中。

MybatisRedisCache.java

package com.foooor.hellomybatis.cache;

import cn.hutool.core.util.StrUtil;
import com.foooor.hellomybatis.config.ApplicationContextHolder;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.cache.Cache;
import org.springframework.data.redis.core.RedisTemplate;

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

@Slf4j
public class MyBatisRedisCache implements Cache {

    // 设置缓存过期时间
    private static final long EXPIRE_TIME_IN_MINUTES = 30;

    private final String id;
    // 实现Cache接口,因为这个类不是Spring管理的,所以通过SpringContextHolder从ioc容器里获取redisTemplate类
    private RedisTemplate<String, Object> redisTemplate;
    private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    public MyBatisRedisCache(String id) {
        if (StrUtil.isEmpty(id)) {
            throw new IllegalArgumentException("cache instances require an id.");
        }
        this.id = id;

        if (redisTemplate == null) {
            // 从容器中获取redisTemplate
            redisTemplate = ApplicationContextHolder.getBean("redisTemplate");
        }
    }

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

    @Override
    public void putObject(Object key, Object value) {
        redisTemplate.opsForValue().set(getCacheKey(key), value, EXPIRE_TIME_IN_MINUTES, TimeUnit.MINUTES);
    }

    @Override
    public Object getObject(Object key) {
        return redisTemplate.opsForValue().get(getCacheKey(key));
    }

    @Override
    public Object removeObject(Object key) {
        Object obj = redisTemplate.opsForValue().get(getCacheKey(key));
        redisTemplate.delete(getCacheKey(key));
        return obj;
    }

    @Override
    public void clear() {
        String cacheKeyPrefix = this.id + ":";
        Set<String> keys = redisTemplate.keys(cacheKeyPrefix + "*");
        if (keys != null && !keys.isEmpty()) {
            redisTemplate.delete(keys);
        }
    }

    @Override
    public int getSize() {
        String cacheKeyPrefix = this.id + ":";
        Set<String> keys = redisTemplate.keys(cacheKeyPrefix + "*");
        return keys != null ? keys.size() : 0;
    }

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

    private String getCacheKey(Object key) {
        return this.id + ":" + key.toString();
    }
}
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

因为上面的 Cache 类不归 Spring 管理,所以通过 ApplicationContextHolder 从 IOC 容器里获取 redisTemplate 类。

下面新建ApplicationContextHolder。

# 14.4 新建ApplicationContextHolder

主要是为了从 Spring 容器中获取 Bean 对象,用来获取 RedisTemplate。

package com.foooor.hellomybatis.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

/**
 * Spring bean的工具类
 */
@Slf4j
@Component
public class ApplicationContextHolder implements ApplicationContextAware, DisposableBean {

    private static ApplicationContext applicationContext;

    /**
     * 获取存储在静态变量中的 ApplicationContext
     */
    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    /**
     * 从applicationContext中获取Bean
     */
    public static <T> T getBean(String name) {
        return (T) applicationContext.getBean(name);
    }

    /**
     * 从applicationContext中获取Bean
     */
    public static <T> T getBean(Class<T> clazz) {
        return applicationContext.getBean(clazz);
    }


    /**
     * 实现 ApplicationContextAware 接口,注入ApplicationContext
     */
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        ApplicationContextHolder.applicationContext = applicationContext;
    }

    /**
     * 实现DisposableBean接口,在Context关闭时清理静态变量
     */
    public void destroy() throws Exception {
        log.debug("清除ApplicationContext: {}", applicationContext);
        applicationContext = null;
    }

}
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

# 14.5 修改实体类

首先要修改实体类,继承 Serializable 接口。

User.java

package com.foooor.hellomybatis.pojo;

import lombok.Data;

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

@Data
public class User implements Serializable {
    private String id;
    private String username;
    private Integer age;
    private Date createTime;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 14.6 修改Mapper接口

修改Mapper接口,添加 @CacheNamespace(implementation = MyBatisRedisCache.class) 注解。

UserMapper.java

@Mapper
@CacheNamespace(implementation = MyBatisRedisCache.class)
public interface UserMapper {

    /**
     * 根据ID查询用户
     */
    @Select("SELECT * FROM tb_user WHERE id = #{id}")
    User selectById(Integer id);

    // ...其他略
}
1
2
3
4
5
6
7
8
9
10
11
12

也可以在 xml 中开始缓存,UserMapper.xml

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

<mapper namespace="com.foooor.hellomybatis.mapper.UserMapper">
    <!-- 开启缓存 -->
    <cache type="com.foooor.hellomybatis.cache.MyBatisRedisCache"></cache>

    <!-- 根据id查询用户 -->
    <select id="selectById" resultType="User">
        SELECT * FROM tb_user
        WHERE id = #{id}
    </select>

    <!-- ...其他略 -->
</mapper>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

上面 注解XML 只能使用一种方式。

这里有一个问题@CacheNamespace 只对使用注解开发的接口生效,<cache type="com.foooor.hellomybatis.cache.MyBatisRedisCache"></cache> 只对 XML 中映射的接口生效,因为通过注解的方式,也不会使用 XML 映射。所以理论上两个都配置就行了,但是同时添加这两个配置又冲突报错

怎么解决?

可以在 Mapper 接口上继续使用 @CacheNamespace

@CacheNamespace(implementation = MyBatisRedisCache.class)
1

而在 XML 中使用 <cache-ref> 标签关联命令空间:

<cache-ref namespace="com.foooor.hellomybatis.mapper.UserMapper"/>
1

# 14.7 测试

访问 controller 中的接口。

UserController.java

@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private IUserService userService;

    /**
     * 获取用户信息
     */
    @GetMapping("/{userId}")
    public User getById(@PathVariable("userId") Integer userId) {
        return userService.getUserById(userId);
    }

    // ...其他略
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

连续访问 3 次:http://localhost:8080/user/1

查看打印的日志:

可以看到只有第一次才执行了 SQL,后面都命中了缓存,而且命中率增加。

# 14.8 项目结构

项目结构如下: