# 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>
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
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>
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();
}
}
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;
}
}
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;
}
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);
// ...其他略
}
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>
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)
而在 XML 中使用 <cache-ref>
标签关联命令空间:
<cache-ref namespace="com.foooor.hellomybatis.mapper.UserMapper"/>
# 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);
}
// ...其他略
}
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 项目结构
项目结构如下: