Spring Boot 中配置 MyBatis 二级缓存(注解版)
MyBatis 二级缓存默认基于内存,重启即失且不适合分布式。本文记录用 Redis 作为 MyBatis 二级缓存的实现(注解版),Spring Boot + MyBatis + Redis。
起步
初始化 Spring Boot 工程,pom.xml 关键依赖:
1<?xml version="1.0" encoding="UTF-8"?>
2<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
4 <modelVersion>4.0.0</modelVersion>
5 <parent>
6 <groupId>org.springframework.boot</groupId>
7 <artifactId>spring-boot-starter-parent</artifactId>
8 <version>2.2.1.RELEASE</version>
9 <relativePath/> <!-- lookup parent from repository -->
10 </parent>
11 <groupId>online.keepon.mybatis</groupId>
12 <artifactId>mybatis-cache</artifactId>
13 <version>0.0.1-SNAPSHOT</version>
14 <name>mybatis-cache</name>
15 <description>Demo project for Spring Boot</description>
16
17 <properties>
18 <java.version>11</java.version>
19 </properties>
20
21 <dependencies>
22 <dependency>
23 <groupId>org.springframework.boot</groupId>
24 <artifactId>spring-boot-starter-web</artifactId>
25 </dependency>
26 <dependency>
27 <groupId>org.mybatis.spring.boot</groupId>
28 <artifactId>mybatis-spring-boot-starter</artifactId>
29 <version>2.1.1</version>
30 </dependency>
31 <dependency>
32 <groupId>org.springframework.boot</groupId>
33 <artifactId>spring-boot-starter-data-redis</artifactId>
34 </dependency>
35 <dependency>
36 <groupId>mysql</groupId>
37 <artifactId>mysql-connector-java</artifactId>
38 <scope>runtime</scope>
39 </dependency>
40 <dependency>
41 <groupId>org.projectlombok</groupId>
42 <artifactId>lombok</artifactId>
43 <optional>true</optional>
44 </dependency>
45 <dependency>
46 <groupId>com.alibaba</groupId>
47 <artifactId>fastjson</artifactId>
48 <version>1.2.61</version>
49 </dependency>
50 <dependency>
51 <groupId>org.springframework.boot</groupId>
52 <artifactId>spring-boot-starter-test</artifactId>
53 <scope>test</scope>
54 <exclusions>
55 <exclusion>
56 <groupId>org.junit.vintage</groupId>
57 <artifactId>junit-vintage-engine</artifactId>
58 </exclusion>
59 </exclusions>
60 </dependency>
61 </dependencies>
62
63 <build>
64 <plugins>
65 <plugin>
66 <groupId>org.springframework.boot</groupId>
67 <artifactId>spring-boot-maven-plugin</artifactId>
68 </plugin>
69 </plugins>
70 </build>
71
72</project>
配置文件
server.port=8990
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/assistant?characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
#Redis配置## Redis数据库索引(默认为0)
spring.redis.database=0
# Redis服务器地址
spring.redis.host=127.0.0.1
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码
spring.redis.password=
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.jedis.pool.max-active=8
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.jedis.pool.max-wait=30000ms
# 连接池中的最大空闲连接
spring.redis.jedis.pool.max-idle=8
# 连接池中的最小空闲连接
spring.redis.jedis.pool.min-idle=1
# 连接超时时间(毫秒)
spring.redis.timeout=6000ms
#mybatis
mybatis.type-aliases-package=online.keepon.mybatis.model
mybatis.mapper-locations=classpath:mapper/*
mybatis.configuration.cache-enabled=true
mybatis.configuration.map-underscore-to-camel-case=true
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8
logging.level.online.keepon.mybatis.mapper=trace
实体类
需要实现 Serializable 接口,避免 Redis 序列化报错:
1@Data
2public class User implements Serializable {
3 private Integer id;
4 private String username;
5}
UserMapper
通过 @CacheNamespace 指定缓存实现为自定义的 RedisCache:
1@Mapper
2@CacheNamespace(implementation = RedisCache.class)
3public interface UserMapper {
4
5 @Select("select * from user where id=#{id} ")
6 User selectById(@Param("id") Integer id);
7
8 /**
9 * flushCache = FlushCachePolicy.TRUE 更新的时候删除缓存
10 */
11 @Options(flushCache = FlushCachePolicy.TRUE)
12 @Update("update user set username=#{username} where id=#{id} ")
13 int update(User user);
14}
RedisCache 实现MyBatis 缓存接口
实现 MyBatis 的 Cache 接口,把查询结果写入 Redis 并设置过期时间:
1import lombok.extern.slf4j.Slf4j;
2import online.keepon.mybatis.utils.ApplicationContextHolder;
3import org.apache.ibatis.cache.Cache;
4import org.springframework.data.redis.core.RedisCallback;
5import org.springframework.data.redis.core.RedisTemplate;
6
7import java.util.Objects;
8import java.util.concurrent.TimeUnit;
9import java.util.concurrent.locks.ReadWriteLock;
10import java.util.concurrent.locks.ReentrantReadWriteLock;
11
12@Slf4j
13public class RedisCache implements Cache {
14 private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(true);
15 private String id;
16 private RedisTemplate<String, Object> redisTemplate = ApplicationContextHolder.getBean("redisTemplate");
17
18 // redis过期时间
19 private static final long EXPIRE_TIME_IN_MINUTES = 30;
20
21 public RedisCache(String id) {
22 if (id == null) {
23 throw new IllegalArgumentException("Cache instances require an ID");
24 }
25 this.id = id;
26 }
27
28 @Override
29 public String getId() {
30 return id;
31 }
32
33 /**
34 * Put query result to redis
35 *
36 * @param key
37 * @param value
38 */
39 @Override
40 @SuppressWarnings("unchecked")
41 public void putObject(Object key, Object value) {
42 System.out.println(key);
43 redisTemplate.opsForValue().set(key.toString(), value, EXPIRE_TIME_IN_MINUTES, TimeUnit.SECONDS);
44 log.debug("Put query result to redis");
45 }
46
47 /**
48 * Get cached query result from redis
49 *
50 * @param key
51 * @return
52 */
53 @Override
54 public Object getObject(Object key) {
55 log.debug("Get cached query result from redis");
56 return redisTemplate.opsForValue().get(key.toString());
57 }
58
59 /**
60 * Remove cached query result from redis
61 *
62 * @param key
63 * @return
64 */
65 @Override
66 public Object removeObject(Object key) {
67 redisTemplate.delete(key.toString());
68 log.debug("Remove cached query result from redis");
69 return null;
70 }
71
72 /**
73 * Clears this cache instance
74 */
75 @Override
76 @SuppressWarnings("unchecked")
77 public void clear() {
78 redisTemplate.execute((RedisCallback) connection -> {
79 connection.flushDb();
80 return null;
81 });
82 log.debug("Clear all the cached query result from redis");
83 }
84
85 @Override
86 public int getSize() {
87 return Objects.requireNonNull(Objects.requireNonNull(redisTemplate.getConnectionFactory()).getConnection().dbSize()).intValue();
88 }
89
90 @Override
91 public ReadWriteLock getReadWriteLock() {
92 return readWriteLock;
93 }
94}
Redis 序列化配置
1import com.fasterxml.jackson.annotation.JsonAutoDetect;
2import com.fasterxml.jackson.annotation.PropertyAccessor;
3import com.fasterxml.jackson.databind.ObjectMapper;
4import online.keepon.mybatis.cache.FastJson2JsonRedisSerializer;
5import org.springframework.context.annotation.Bean;
6import org.springframework.context.annotation.Configuration;
7import org.springframework.data.redis.connection.RedisConnectionFactory;
8import org.springframework.data.redis.core.RedisTemplate;
9import org.springframework.data.redis.serializer.StringRedisSerializer;
10
11@Configuration
12public class RedisConfig {
13 @Bean
14 public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
15 RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
16 redisTemplate.setConnectionFactory(redisConnectionFactory);
17 FastJson2JsonRedisSerializer fastJson2JsonRedisSerializer = new FastJson2JsonRedisSerializer(Object.class);
18 ObjectMapper objectMapper = new ObjectMapper();
19 objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
20 objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
21 fastJson2JsonRedisSerializer.setObjectMapper(objectMapper);
22
23 StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
24 // key采用String的序列化方式
25 redisTemplate.setKeySerializer(stringRedisSerializer);
26 // string的value采用fastJson序列化方式
27 redisTemplate.setValueSerializer(fastJson2JsonRedisSerializer);
28 // hash的key也采用String的序列化方式
29 redisTemplate.setHashKeySerializer(stringRedisSerializer);
30 // hash的value采用fastJson序列化方式
31 redisTemplate.setHashValueSerializer(fastJson2JsonRedisSerializer);
32 redisTemplate.afterPropertiesSet();
33 return redisTemplate;
34 }
35}
1import com.alibaba.fastjson.JSON;
2import com.alibaba.fastjson.parser.ParserConfig;
3import com.alibaba.fastjson.serializer.SerializerFeature;
4import com.fasterxml.jackson.databind.JavaType;
5import com.fasterxml.jackson.databind.ObjectMapper;
6import com.fasterxml.jackson.databind.type.TypeFactory;
7import org.springframework.data.redis.serializer.RedisSerializer;
8import org.springframework.data.redis.serializer.SerializationException;
9import org.springframework.util.Assert;
10
11import java.nio.charset.Charset;
12
13public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T> {
14
15 private ObjectMapper objectMapper = new ObjectMapper();
16 public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
17
18 private Class<T> clazz;
19
20 static {
21 ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
22 ParserConfig.getGlobalInstance().addAccept("com.openailab.oascloud");
23
24 }
25
26 public FastJson2JsonRedisSerializer(Class<T> clazz) {
27 super();
28 this.clazz = clazz;
29 }
30
31 @Override
32 public byte[] serialize(T t) throws SerializationException {
33 if (t == null) {
34 return new byte[0];
35 }
36 return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
37 }
38
39 @Override
40 public T deserialize(byte[] bytes) throws SerializationException {
41 if (bytes == null || bytes.length <= 0) {
42 return null;
43 }
44 String str = new String(bytes, DEFAULT_CHARSET);
45
46 return JSON.parseObject(str, clazz);
47 }
48
49 public void setObjectMapper(ObjectMapper objectMapper) {
50 Assert.notNull(objectMapper, "'objectMapper' must not be null");
51 this.objectMapper = objectMapper;
52 }
53
54 protected JavaType getJavaType(Class<?> clazz) {
55 return TypeFactory.defaultInstance().constructType(clazz);
56 }
57}
ApplicationContextHolder
需要注意的是,RedisCache 里不能通过 @Autowired 引用 redisTemplate——因为 RedisCache 并不是 Spring 容器里的 bean,MyBatis 实例化它时不会做依赖注入。所以需要手动调用容器的 getBean 方法来拿到 bean:
1import org.springframework.beans.BeansException;
2import org.springframework.context.ApplicationContext;
3import org.springframework.context.ApplicationContextAware;
4import org.springframework.stereotype.Component;
5
6@Component
7public class ApplicationContextHolder implements ApplicationContextAware {
8 private static ApplicationContext applicationContext;
9
10 @Override
11 public void setApplicationContext(ApplicationContext ctx) throws BeansException {
12 applicationContext = ctx;
13 }
14
15 public static ApplicationContext getApplicationContext() {
16 return applicationContext;
17 }
18
19 public static <T> T getBean(Class<T> clazz) {
20 return applicationContext.getBean(clazz);
21 }
22
23 @SuppressWarnings("unchecked")
24 public static <T> T getBean(String name) {
25 return (T) applicationContext.getBean(name);
26 }
27}
测试
1@SpringBootTest
2class MybatisCacheApplicationTests {
3 @Autowired
4 UserMapper userMapper;
5
6 @Test
7 void select() {
8 User user = userMapper.selectById(1);
9 System.out.println(user);
10 }
11
12 @Test
13 public void update() {
14 User user = new User();
15 user.setId(1);
16 user.setUsername("user");
17 userMapper.update(user);
18 }
19}
执行 select 后再查询走 Redis 缓存;执行 update 时 @Options(flushCache = FlushCachePolicy.TRUE) 会清掉对应缓存,下次查询重新加载。
