MyBatis Plus integrates Redis to implement distributed second-level cache

MyBatis cache description

MyBatis provides two levels of cache, namely first-level cache and second-level cache. The first-level cache is a SqlSession-level cache. It only stores cached data inside the SqlSession object. If the SqlSession objects are different, the cache cannot be hit. The second-level cache is a mapper-level cache. As long as the Mapper class used is the same, the cache can be shared.

When querying data, Mybatis will first query the second-level cache. If the second-level cache is not available, it will query the first-level cache. If it is not available, the database query will be performed.

Mybatis’s first-level cache is enabled by default, while the second-level cache needs to be enabled manually in the mapper.xml configuration file or through the @CacheNamespace annotation.

It should be noted that when Spring is integrated, the transaction first-level cache must be enabled to take effect, because if the cache is not enabled, a SqlSession object will be re-created for each query, so the cache cannot be shared.

Enable the second-level cache of a Mapper through @CacheNamespace.

@Mapper
@CacheNamespace
public interface EmployeeMapper extends BaseMapper<Employee> {<!-- -->
}

Enable all second-level caches:

mybatis-plus:
    mapper-locations: classpath:mybatis/mapper/*.xml
    configuration:
      cache-enabled: true

MybatisPlus integrates Redis to implement distributed second-level cache

The built-in secondary cache of Mybatis has distribution problems in a distributed environment and cannot be used. However, we can integrate Redis to implement distributed secondary cache.

1.Introduce dependencies

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.4.1</version>
</dependency>

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

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.24.3</version>
</dependency>

<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.8.22</version>
</dependency>

2. Configure RedisTemplate

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;

@Configuration
@EnableCaching
public class RedisConfiguration {<!-- -->
    private static final StringRedisSerializer STRING_SERIALIZER = new StringRedisSerializer();
    private static final GenericJackson2JsonRedisSerializer JACKSON__SERIALIZER = new GenericJackson2JsonRedisSerializer();


    @Bean
    @Primary
    public CacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {<!-- -->
        //Set cache expiration time
        RedisCacheConfiguration redisCacheCfg = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofHours(1))
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(STRING_SERIALIZER))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(JACKSON__SERIALIZER));
        return RedisCacheManager.builder(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory))
                .cacheDefaults(redisCacheCfg)
                .build();
    }

    @Bean
    @Primary
    @ConditionalOnMissingBean(name = "redisTemplate")
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {<!-- -->
        //Configure redisTemplate
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(factory);
        // key serialization
        redisTemplate.setKeySerializer(STRING_SERIALIZER);
        // value serialization
        redisTemplate.setValueSerializer(JACKSON__SERIALIZER);
        // Hash key serialization
        redisTemplate.setHashKeySerializer(STRING_SERIALIZER);
        // Hash value serialization
        redisTemplate.setHashValueSerializer(JACKSON__SERIALIZER);
        // Set up support transactions
        redisTemplate.setEnableTransactionSupport(true);

        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

    @Bean
    public RedisSerializer<Object> redisSerializer() {<!-- -->
        //Create JSON serializer
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        //Must be set, otherwise JSON cannot be converted into an object and will be converted into a Map type
        objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
        return new GenericJackson2JsonRedisSerializer(objectMapper);
    }
}

3. Custom cache class

import cn.hutool.extra.spring.SpringUtil;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.cache.Cache;
import org.redisson.api.RReadWriteLock;
import org.redisson.api.RedissonClient;
import org.springframework.data.redis.connection.RedisServerCommands;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;

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


@Slf4j
public class MybatisRedisCache implements Cache {<!-- -->

    // redisson read-write lock
    private final RReadWriteLock redissonReadWriteLock;
    // redisTemplate
    private final RedisTemplate redisTemplate;
    // cache ID
    private final String id;
    //Expiration time 10 minutes
    private final long expirationTime = 1000*60*10;

    public MybatisRedisCache(String id) {<!-- -->
        this.id = id;
        //Get redisTemplate
        this.redisTemplate = SpringUtil.getBean(RedisTemplate.class);
        //Create a read-write lock
        this.redissonReadWriteLock = SpringUtil.getBean(RedissonClient.class).getReadWriteLock("mybatis-cache-lock:" + this.id);
    }


    @Override
    public void putObject(Object key, Object value) {<!-- -->
        //Use the Hash type of redis for storage
        redisTemplate.opsForValue().set(getCacheKey(key),value,expirationTime, TimeUnit.MILLISECONDS);
    }

    @Override
    public Object getObject(Object key) {<!-- -->
        try {<!-- -->
            //Get data from redis based on key
            Object cacheData = redisTemplate.opsForValue().get(getCacheKey(key));

            log.debug("[Mybatis second-level cache] Query cache,cacheKey={},data={}",getCacheKey(key), JSONUtil.toJsonStr(cacheData));

            return cacheData;
        } catch (Exception e) {<!-- -->
            log.error("Caching error",e);
        }
        return null;
    }

    @Override
    public Object removeObject(Object key) {<!-- -->
        if (key != null) {<!-- -->
            log.debug("[Mybatis second-level cache] delete cache,cacheKey={}",getCacheKey(key));
            redisTemplate.delete(key.toString());
        }
        return null;
    }

    @Override
    public void clear() {<!-- -->
        log.debug("[Mybatis second-level cache] Clear cache, id={}",getCachePrefix());
        Set keys = redisTemplate.keys(getCachePrefix() + ":*");
        redisTemplate.delete(keys);
    }

    @Override
    public int getSize() {<!-- -->
        Long size = (Long) redisTemplate.execute((RedisCallback<Long>) RedisServerCommands::dbSize);
        return size.intValue();
    }

    @Override
    public ReadWriteLock getReadWriteLock() {<!-- -->
        return this.redissonReadWriteLock;
    }

    @Override
    public String getId() {<!-- -->
        return this.id;
    }

    public String getCachePrefix(){<!-- -->
        return "mybatis-cache:%s".formatted(this.id);
    }
    private String getCacheKey(Object key){<!-- -->
        return getCachePrefix() + ":" + key;
    }

}

4. Enable secondary cache on the Mapper interface

//Enable the second level cache and specify the cache class
@CacheNamespace(implementation = MybatisRedisCache.class,eviction = MybatisRedisCache.class)
@Mapper
public interface EmployeeMapper extends BaseMapper<Employee> {<!-- -->
}