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) 会清掉对应缓存,下次查询重新加载。

参考