源本科技 | 码上会

Spring Boot 开启二级缓存

2026/04/29
2
0

引言

一级缓存

一级缓存是 MyBatis 默认开启的缓存机制,存在于内存中,也称为本地缓存。它是 SqlSession 级别的缓存,无法关闭,但可以通过多种方式清空缓存。

  1. 客户端想要获取数据,就把这个请求发送给 MyBatis

  2. MyBatis 收到请求后,直接把任务交给 SQL Session 来处理

  3. SQL Session 首先会检查自己手里的一级缓存,看有没有之前查过的数据

  4. 如果缓存里正好有需要的数据,SQL Session 就直接从缓存里拿出来,快速返回给 MyBatis,然后再传回给客户端

  5. 如果缓存里没有找到需要的数据,SQL Session 就得去数据库查了。查完数据后,它会把结果保存到缓存里,以备下次查询用。然后,再把结果发回给 MyBatis,最终返回给客户端

二级缓存

MyBatis 默认未开启二级缓存,需配置以启用,二级缓存由多个 SqlSession 共享,作用于相同 mapper namespace。当相同参数下的相同 SQL 在不同 SqlSession 中重复执行时,首次结果会写入缓存,后续直接从缓存读取以提高查询效率。

  1. 客户端发起查询请求

  2. MyBatis 框架处理请求并将任务分派给 SQL Session

  3. SQL Session 首先会检查一级缓存,如果缓存命中则直接返回结果

  4. MyBatis 一级缓存(与 SQL Session 生命周期一致)

  5. 二级缓存(例如 Redis),如果一级缓存未命中,SQL Session 会检查二级缓存

  6. 如果 Redis 中存在数据,返回结果并将其保存到一级缓存

  7. 如果 Redis 中也没有数据,则执行数据库查询,将结果保存到一级缓存和二级缓存(Redis)中

  8. 如果两级缓存都未命中,SQL Session 最终会从数据库查询


开启 MyBatis 二级缓存

Redis 工具类

封装 Redis 常用操作,包含 Set、Hash、List、String 等数据结构方法:

package com.lusifer.crmeb.util;

import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.CollectionUtils;

import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 * Redis 工具类, 用于简化 Redis 操作。
 */
public final class RedisUtils {

    private final RedisTemplate<String, Object> redisTemplate;

    public RedisUtils(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    /**
     * 执行 Redis 回调操作
     */
    public <T> T execute(RedisCallback<T> action) {
        return redisTemplate.execute(action);
    }

    /**
     * 删除多个键
     */
    public Long del(Collection<String> keys) {
        if (CollectionUtils.isEmpty(keys)) {
            return 0L;
        }
        return redisTemplate.delete(keys);
    }

    /**
     * 通过模式查询键
     */
    public Set<String> keys(String pattern) {
        return redisTemplate.keys(pattern);
    }

    /**
     * 发布消息到指定频道
     */
    public void publish(String channel, Object message) {
        redisTemplate.convertAndSend(channel, message);
    }

    /**
     * 如果键不存在,则设置键的值并设置过期时间
     */
    public boolean setIfAbsent(String key, Object value, long time) {
        return time > 0 && Boolean.TRUE.equals(redisTemplate.opsForValue().setIfAbsent(key, value, time, TimeUnit.SECONDS));
    }

    /**
     * 如果键不存在,则设置键的值
     */
    public boolean setIfAbsent(String key, Object value) {
        Boolean result = redisTemplate.opsForValue().setIfAbsent(key, value);
        return result != null && result;
    }

    /**
     * 设置键的过期时间
     */
    public boolean expire(String key, long time) {
        return time > 0 && redisTemplate.expire(key, time, TimeUnit.SECONDS);
    }

    /**
     * 获取键的剩余过期时间
     */
    public long getExpire(String key) {
        return redisTemplate.getExpire(key, TimeUnit.SECONDS);
    }

    /**
     * 判断键是否存在
     */
    public boolean hasKey(String key) {
        return redisTemplate.hasKey(key);
    }

    /**
     * 删除多个键
     */
    public void del(String... keys) {
        redisTemplate.delete(Arrays.asList(keys));
    }

    // ============================ String Operations ===========================

    /**
     * 获取字符串值
     */
    public Object get(String key) {
        return redisTemplate.opsForValue().get(key);
    }

    /**
     * 设置字符串值并设置过期时间
     */
    public boolean set(String key, Object value, long time) {
        if (time > 0) {
            redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
        } else {
            redisTemplate.opsForValue().set(key, value);
        }

        return redisTemplate.hasKey(key);
    }

    /**
     * 递增操作
     */
    public long incr(String key, long delta) {
        if (delta < 0) {
            throw new IllegalArgumentException("Increment factor must be greater than 0");
        }
        Long result = redisTemplate.opsForValue().increment(key, delta);
        return result != null ? result : 0;
    }

    /**
     * 递减操作
     */
    public long decr(String key, long delta) {
        if (delta < 0) {
            throw new IllegalArgumentException("Decrement factor must be greater than 0");
        }
        Long result = redisTemplate.opsForValue().increment(key, -delta);
        return result != null ? result : 0;
    }

    // ============================ Hash Operations ===========================

    /**
     * 获取哈希表中的字段值
     */
    public Object hGet(String key, String field) {
        return redisTemplate.opsForHash().get(key, field);
    }

    /**
     * 获取哈希表中的所有字段值
     */
    public Map<Object, Object> hGetAll(String key) {
        return redisTemplate.opsForHash().entries(key);
    }

    /**
     * 设置哈希表中的字段值并设置过期时间
     */
    public boolean hSetAll(String key, Map<String, Object> map, long time) {
        redisTemplate.opsForHash().putAll(key, map);
        return time > 0 && expire(key, time);
    }

    /**
     * 设置哈希表中的单个字段值
     */
    public boolean hSet(String key, String field, Object value) {
        redisTemplate.opsForHash().put(key, field, value);
        return true;
    }

    /**
     * 删除哈希表中的字段
     */
    public void hDel(String key, Object... fields) {
        redisTemplate.opsForHash().delete(key, fields);
    }

    /**
     * 判断哈希表中是否包含指定字段
     */
    public boolean hHasKey(String key, String field) {
        return redisTemplate.opsForHash().hasKey(key, field);
    }

    // ============================ Set Operations ===========================

    /**
     * 获取集合中的所有值
     */
    public Set<Object> sGet(String key) {
        return redisTemplate.opsForSet().members(key);
    }

    /**
     * 判断集合中是否包含指定的值
     */
    public boolean sHasKey(String key, Object value) {
        Boolean result = redisTemplate.opsForSet().isMember(key, value);
        return result != null && result;
    }

    /**
     * 向集合中添加一个或多个值
     */
    public long sAdd(String key, Object... values) {
        Long count = redisTemplate.opsForSet().add(key, values);
        return count != null ? count : 0;
    }

    /**
     * 获取集合的大小
     */
    public long sSize(String key) {
        Long size = redisTemplate.opsForSet().size(key);
        return size != null ? size : 0;
    }

    /**
     * 从集合中移除指定值
     */
    public long sRemove(String key, Object... values) {
        Long count = redisTemplate.opsForSet().remove(key, values);
        return count != null ? count : 0;
    }

    // ============================ List Operations ===========================

    /**
     * 获取列表中的元素
     */
    public List<Object> lRange(String key, long start, long end) {
        return redisTemplate.opsForList().range(key, start, end);
    }

    /**
     * 获取列表的长度
     */
    public long lSize(String key) {
        Long size = redisTemplate.opsForList().size(key);
        return size != null ? size : 0;
    }

    /**
     * 向列表右侧添加元素并设置过期时间
     */
    public boolean lPush(String key, Object value, long time) {
        redisTemplate.opsForList().rightPush(key, value);
        return time > 0 && expire(key, time);
    }

    /**
     * 从列表中移除指定数量的元素
     */
    public long lRemove(String key, long count, Object value) {
        Long result = redisTemplate.opsForList().remove(key, count, value);
        return result != null ? result : 0;
    }
}

自定义 MyBatis 缓存实现

集成 Hutool 工具类,实现 Redis 二级缓存:

<!-- Hutool 工具类 -->
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.8.26</version>
</dependency>
package com.lusifer.crmeb.cache;

import cn.hutool.extra.spring.SpringUtil;
import com.lusifer.crmeb.util.RedisUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.cache.Cache;
import org.springframework.data.redis.connection.RedisServerCommands;
import org.springframework.util.DigestUtils;

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

/**
 * MyBatis 与 Redis 集成的二级缓存实现
 */
@Slf4j
public class MybatisPlusRedisCache implements Cache {

    private RedisUtils redisUtil;
    private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(true);
    private final String id;

    public MybatisPlusRedisCache(final String id) {
        if (id == null) {
            throw new IllegalArgumentException("Cache instances require an ID");
        }
        log.info("二级缓存 ID:{}", id);
        this.id = id;
    }

    private void getRedisUtil() {
        if (redisUtil == null) {
            redisUtil = SpringUtil.getBean("redisUtils");
        }
    }

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

    @Override
    public void putObject(Object key, Object value) {
        log.info("存入缓存");
        getRedisUtil();
        try {
            redisUtil.hSet(id, MD5Encrypt(key), value);
        } catch (Exception e) {
            log.error("存入缓存失败", e);
        }
    }

    @Override
    public Object getObject(Object key) {
        log.info("获取缓存");
        getRedisUtil();
        try {
            return redisUtil.hGet(id, MD5Encrypt(key));
        } catch (Exception e) {
            log.error("获取缓存失败", e);
        }
        return null;
    }

    @Override
    public Object removeObject(Object key) {
        log.info("删除缓存");
        getRedisUtil();
        try {
            redisUtil.del(MD5Encrypt(key));
        } catch (Exception e) {
            log.error("删除缓存失败", e);
        }
        return null;
    }

    @Override
    public void clear() {
        log.info("清空缓存");
        getRedisUtil();
        try {
            redisUtil.del(id);
        } catch (Exception e) {
            log.error("清空缓存失败", e);
        }
    }

    @Override
    public int getSize() {
        getRedisUtil();
        Long size = redisUtil.execute(RedisServerCommands::dbSize);
        return size != null ? size.intValue() : 0;
    }

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

    /**
     * MD5 加密 key,缩短长度
     */
    private String MD5Encrypt(Object key) {
        return DigestUtils.md5DigestAsHex(key.toString().getBytes());
    }
}

Redis 配置类

注册 Redis 工具类 Bean:

@Bean(name = "redisUtils")
public RedisUtils redisUtils(final RedisTemplate<String, Object> redisTemplate) {
    return new RedisUtils(redisTemplate);
}

配置文件

开启 MyBatis 二级缓存:

mybatis-plus:
  type-aliases-package: com.lusifer.crmeb.entity
  configuration:
    cache-enabled: true

Mapper 接口

启用自定义 Redis 二级缓存:

import com.lusifer.crmeb.cache.MybatisPlusRedisCache;
import com.lusifer.crmeb.entity.EbArticle;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.CacheNamespace;
import org.apache.ibatis.annotations.Mapper;

@Mapper
@CacheNamespace(implementation = MybatisPlusRedisCache.class, eviction = MybatisPlusRedisCache.class)
public interface EbArticleMapper extends BaseMapper<EbArticle> {

}

缓存测试

  • 首次运行:查询缓存无数据,则查询数据并存入缓存

  • 再次运行:先查询缓存,发现有数据直接返回数据

  • Redis 客户端效果图


补充说明

LocalDateTime

问题描述

存储包含 LocalDateTime 类型的对象到 Redis 时,会出现序列化异常:

org.springframework.data.redis.serializer.SerializationException: Could not write JSON: Java 8 date/time type java.time.LocalDateTime not supported by default

解决方案

  • 添加 Jackson 日期模块依赖

<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-jsr310</artifactId>
    <version>2.15.3</version>
</dependency>
  • 实体类添加日期格式化注解

@JsonSerialize(using = LocalDateTimeSerializer.class)
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime createTime;

序列化与反序列化

序列化是将对象转换为可存储 / 传输的格式(如 JSON)的过程;反序列化是将数据还原为 Java 对象的过程。

序列化测试

@Test
public void testSerial() {
    User user = new User();
    user.setName("张三");
    user.setAge(18);

    ObjectMapper objectMapper = new ObjectMapper();
    String json = objectMapper.writeValueAsString(user);
    System.out.println(json);
}

反序列化测试

@Test
public void testDeSerial() {
    String json = "{\"name\":\"张三\",\"age\":18}";

    ObjectMapper objectMapper = new ObjectMapper();
    User user = objectMapper.readValue(json, User.class);
    System.out.println(user);
}