MyBatis 整合 Redis 作为二级缓存

前言

为了配个缓存我容易吗我….

最近在写一个个人项目, 用 Spring Boot 构建, 因为省事就用了 Myatis 和 MyBatis Generator 管理我的 SQL 语句. 昨天心血来潮想用缓存减轻数据库压力(因为某些特定页面信息量比较多,所以SQL查询比较慢). 想了想觉得还是不用 MyBatis 自带的二级缓存(HashMap 实现的, 感觉不是很好), 最后选择了 Redis 作为缓存方案.

但是 MyBatis 整合 Redis 这个东西, 网上的坑是真的多阿… 还都是相互抄的… 偏偏 MyBatis 的官方文档对于二级缓存这一部分资料又比较少, 基本上就是一笔带过. 因为还要肝项目,所以就直接 Baid/Google 各种解决方案, 最后还是在掘金找到了个能用的,下面附上参考链接

当然因为我是用的 MyBatis Generator, 所以生成的时候没法序列化, 翻了好久,最后才发现官方已经给了序列化的插件

开始

这里我附上 import, 缺的包自行下载. 另外,日志我使用的是 @Slf4j 注解,这个需要搭配 IDEA 的 Lombook插件使用(Eclipse我不确定有没有),如果不想安装的话可以自行参考上面我所附参考链接的日志写法

首先,我们需要实现 org.apache.ibatis.cache.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
106
107
108
109
110
111
112
113
114
115
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.cache.Cache;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;

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

/**
* @ClassName RedisCache
* @Description Mybatis 二级缓存实现
* @Author bestsort
* @Date 19-10-2 上午10:15
* @Version 1.0
*/


@Slf4j
public class RedisCache implements Cache {

private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
/**
* id,每个实现 Cache 的类都需要实现带 id 的构造方法
*/
private final String id;
private RedisTemplate redisTemplate;
/**
* redis过期时间
*/
private static final long EXPIRE_TIME_IN_MINUTES = 30;


public RedisCache(String id) {
if (id == null) {
throw new IllegalArgumentException("Cache instances require an ID");
}
this.id = id;
}

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

@Override
@SuppressWarnings("unchecked")
public void putObject(Object key, Object value) {
try {
RedisTemplate redisTemplate = getRedisTemplate();
ValueOperations opsForValue = redisTemplate.opsForValue();
opsForValue.set(key, value, EXPIRE_TIME_IN_MINUTES, TimeUnit.MINUTES);
log.debug("Put query result to redis");
}
catch (Throwable t) {
log.error("Redis put failed", t);
}
}

@Override
public Object getObject(Object key) {
try {
RedisTemplate redisTemplate = getRedisTemplate();
ValueOperations opsForValue = redisTemplate.opsForValue();
log.debug("Get cached query result from redis");
return opsForValue.get(key);
}
catch (Throwable t) {
log.error("Redis get failed, fail over to db", t);
return null;
}
}

@Override
@SuppressWarnings("unchecked")
public Object removeObject(Object key) {
try {
RedisTemplate redisTemplate = getRedisTemplate();
redisTemplate.delete(key);
log.debug("Remove cached query result from redis");
}
catch (Throwable t) {
log.error("Redis remove failed", t);
}
return null;
}

@Override
public void clear() {
RedisTemplate redisTemplate = getRedisTemplate();
redisTemplate.execute((RedisCallback) connection -> {
connection.flushDb();
return null;
});
log.debug("Clear all the cached query result from redis");
}

@Override
public int getSize() {
return 0;
}

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

private RedisTemplate getRedisTemplate() {
if (redisTemplate == null) {
redisTemplate = ApplicationContextHolder.getBean("redisTemplate");
}
return redisTemplate;
}
}

需要注意的是,这里不能通过 @Autowire 的方式引用redisTemplate,因为RedisCache并不是Spring容器里的bean。所以我们需要手动地去调用容器的getBean方法来拿到这个bean,所以需要再创建一个类拿到 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
package cn.bestsort.bbslite.cache;

/**
* @ClassName ApplicationContextHolder
* @Author bestsort
* @Date 19-10-2 上午10:22
* @Version 1.0
*/

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

@Component
public class ApplicationContextHolder implements ApplicationContextAware {
private static ApplicationContext applicationContext;

@Override
public void setApplicationContext(ApplicationContext ctx) throws BeansException {
applicationContext = ctx;
}

/**
* Get application context from everywhere
*
* @return
*/
public static ApplicationContext getApplicationContext() {
return applicationContext;
}

/**
* Get bean by class
*
* @param clazz
* @param <T>
* @return
*/
public static <T> T getBean(Class<T> clazz) {
return applicationContext.getBean(clazz);
}

/**
* Get bean by class name
*
* @param name
* @param <T>
* @return
*/
@SuppressWarnings("unchecked")
public static <T> T getBean(String name) {
return (T) applicationContext.getBean(name);
}
}

序列化

当然, 光上面是不够的,还需要在 数据库的查询对象 里实现 Serializable 接口. 拿我项目中的 topic 类举例,需要像这样:

未实现 Serializable

1
2
3
4
5
6
7
8
9
10
11
12
13
14

public class Topic {
private Long id;

private String name;

private String avatarUrl;

private Long questionCount;

private Long followCount;

//getter 和 setter
}

实现后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Topic implements Serializable {
private Long id;

private String name;

private String avatarUrl;

private Long questionCount;

private Long followCount;
private static final long serialVersionUID = 1L;

//getter 和 setter
}

实现了 Serializable 接口后,我们可以在 xxxMapper.xml 中添加字段来使用二级缓存了.

MyBatis Generator

因为我用了 MyBatis Generator,所以不可能手动去写的, 这样每次重新生成 mapper 文件的时候我还得手动改,不存在的.

幸运的是,官方已经为我们提供了 自动序列化的插件,所以只需要添加插件就可以了. 在 generatorConfiguration 字段下的 context 里添加两个插件

1
2
3
4
5
6
7
8
9
10
<!-- 序列化插件 -->
<plugin type="org.mybatis.generator.plugins.SerializablePlugin"/>
<!-- 所有Mapper 都开启缓存 -->

<plugin type="org.mybatis.generator.plugins.CachePlugin">
<property name="cache_type" value="cn.bestsort.bbslite.cache.RedisCache"/>
<property name="cache_eviction" value="LRU"/>
<property name="cache_flushInterval" value="60000"/>
<property name="cache_size" value="512"/>
</plugin>

然后再执行一次 mvn -Dmybatis.generator.overwrite=true mybatis-generator:generate 重新生成并覆盖生成的文件即可

测试

运行一下我的项目,下面这是第一次访问

第二次访问相同内容,可以在日志中看到缓存已经命中

觉得文章不错的话可以请我喝一杯茶哟~
  • 本文作者: bestsort
  • 本文链接: https://bestsort.cn/2019/10/02/102/
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-SA 许可协议。转载请注明出处!并保留本声明。感谢您的阅读和支持!
-------------本文结束感谢您的阅读-------------