springboot +mybatis 查询myql开启程序级别缓存

在 Spring Boot + MyBatis 架构中,所谓的"程序级别缓存"(应用层缓存)通常有两种实现方式:

  1. MyBatis 自带的二级缓存(Mapper 级别,作用于数据访问层)。
  2. Spring Cache 缓存抽象 (Service 级别,结合 Redis/Caffeine 等,企业级开发最推荐)。

(注:MySQL 8.0 已经彻底移除了数据库自身的 Query Cache,因此将缓存前置到程序层是标准做法。)

下面为您详细介绍这两种方案的开启和配置方法。


方案一:开启 MyBatis 二级缓存(Mapper 级别)

MyBatis 一级缓存(SqlSession 级别)是默认开启的。二级缓存是跨 SqlSession 的,作用于同一个 Mapper 的 namespace。

1. 开启全局配置

application.ymlapplication.properties 中开启 MyBatis 的二级缓存支持:

复制代码
复制代码
mybatis:
  configuration:
    # 开启 MyBatis 二级缓存(Spring Boot 中默认通常为 true,但建议显式声明)
    cache-enabled: true
2. 在 Mapper 接口或 XML 中开启缓存

方式 A:使用注解(推荐) 在 Mapper 接口上添加 @CacheNamespace 注解:

复制代码
复制代码
import org.apache.ibatis.annotations.CacheNamespace;
import org.apache.ibatis.annotations.Mapper;

@Mapper
@CacheNamespace(eviction = org.apache.ibatis.cache.decorators.LruCache.class, size = 1024)
public interface UserMapper {
    User selectById(Long id);
}

方式 B:使用 XML 配置 在对应的 Mapper XML 文件中添加 <cache/> 标签:

复制代码
复制代码
<mapper namespace="com.example.mapper.UserMapper">
    <!-- 开启二级缓存,并配置淘汰策略、刷新间隔、最大引用数等 -->
    <cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/>
    
    <select id="selectById" resultType="com.example.entity.User">
        SELECT * FROM user WHERE id = #{id}
    </select>
</mapper>
3. 实体类实现序列化

因为二级缓存可能会将对象序列化到磁盘或进行深拷贝,所以返回的实体类必须实现 Serializable 接口

复制代码
复制代码
import java.io.Serializable;

public class User implements Serializable {
    private static final long serialVersionUID = 1L;
    private Long id;
    private String name;
    // getters and setters
}
⚠️ MyBatis 二级缓存的"坑"与注意事项
  • 脏读问题 :MyBatis 二级缓存是基于 namespace 的。如果 UserMapperOrderMapper 都关联了 user 表,当 OrderMapper 更新了 user 数据时,UserMapper 的二级缓存不会自动失效,导致脏读。
  • 分布式问题 :在微服务或多实例部署下,MyBatis 默认的二级缓存是单机内存缓存,会导致各个节点数据不一致(除非整合 Redis 等第三方二级缓存插件,如 mybatis-redis)。
  • 结论在复杂的生产环境中,通常建议关闭 MyBatis 二级缓存,改用方案二。

方案二:使用 Spring Cache(Service 级别,🌟 强烈推荐)

在实际企业开发中,我们通常关闭 MyBatis 二级缓存,转而在 Service 层 使用 Spring 提供的 @Cacheable 注解,结合 Caffeine(本地内存)Redis(分布式) 来实现程序级缓存。

1. 引入依赖

以使用本地高性能缓存 Caffeine 为例(如果需要分布式,可替换为 spring-boot-starter-data-redis):

复制代码
复制代码
<!-- Spring Cache 核心依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- Caffeine 本地缓存实现 -->
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
</dependency>
2. 开启缓存支持

在 Spring Boot 启动类上添加 @EnableCaching 注解:

复制代码
复制代码
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;

@SpringBootApplication
@EnableCaching // 开启 Spring 缓存注解支持
public class MyApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }
}
3. 配置缓存参数 (application.yml)
复制代码
复制代码
spring:
  cache:
    type: caffeine
    caffeine:
      spec: maximumSize=10000,expireAfterWrite=60s # 最大10000条,写入后60秒过期
    cache-names:
      - users
      - orders
4. 在 Service 层使用注解

在查询方法上使用 @Cacheable,在更新/删除方法上使用 @CacheEvict 清除缓存:

复制代码
复制代码
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    /**
     * 查询时走缓存
     * value: 缓存名称
     * key: 缓存的 key,使用 SpringEL 表达式
     */
    @Cacheable(value = "users", key = "#id")
    public User getUserById(Long id) {
        // 如果缓存中有,直接返回,不会执行下面的数据库查询代码
        return userMapper.selectById(id); 
    }

    /**
     * 更新时清除缓存
     */
    @CacheEvict(value = "users", key = "#user.id")
    public void updateUser(User user) {
        userMapper.updateById(user);
    }
    
    /**
     * 清除该 namespace 下的所有缓存
     */
    @CacheEvict(value = "users", allEntries = true)
    public void clearAllUserCache() {
        // ...
    }
}

总结与建议

对比维度 MyBatis 二级缓存 (方案一) Spring Cache + Caffeine/Redis (方案二)
作用层级 Mapper 层 (DAO层) Service 层 (业务逻辑层)
控制粒度 较粗(基于 Namespace) 极细(基于方法、参数自定义 Key)
多表关联查询 极易产生脏数据(不推荐) 可通过手动清除或设置合理过期时间解决
分布式支持 默认不支持(需引入第三方插件) 完美支持(直接切换为 Redis 即可)
生产环境推荐度 ⭐ (仅适合单表、不常更新的字典表) ⭐⭐⭐⭐⭐ (企业级标准做法)

最佳实践建议:

  1. application.yml 中设置 mybatis.configuration.cache-enabled: false 关闭 MyBatis 二级缓存。
  2. 引入 Spring Cache,如果是单机应用使用 Caffeine ,如果是微服务/集群应用使用 Redis
  3. 把缓存逻辑写在 Service 层 ,通过 @Cacheable@CacheEvict 精确控制数据的读取和失效。

使用redis,怎么配置

在 Spring Boot 中使用 Spring Cache + Redis 是企业级开发中最标准、最推荐的分布式缓存方案。

使用 Redis 作为缓存实现时,最大的痛点是"序列化乱码"问题(Spring 默认使用 JDK 序列化,会导致 Redis 里的 Key 和 Value 变成不可读的二进制乱码)。

下面是完整、可直接用于生产环境的配置指南,包含连接配置、自定义序列化、按模块设置过期时间等核心步骤。


第一步:引入 Maven 依赖

复制代码
复制代码
<!-- Spring Cache 核心 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- Spring Data Redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Lettuce 连接池支持 (Spring Boot 默认使用 Lettuce) -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>

第二步:配置 Redis 连接信息 (application.yml)

注意 :Spring Boot 2.x 使用 spring.redis,Spring Boot 3.x 使用 spring.data.redis。以下以 3.x / 2.4+ 为准。

复制代码
复制代码
spring:
  data:
    redis:
      host: 127.0.0.1
      port: 6379
      password: your_password  # 如果没有密码请留空或删除此行
      database: 0
      timeout: 5000ms
      lettuce:
        pool:
          max-active: 16      # 最大连接数
          max-idle: 8         # 最大空闲连接
          min-idle: 2         # 最小空闲连接
          max-wait: 2000ms    # 连接阻塞最大等待时间

  cache:
    type: redis
    redis:
      time-to-live: 600000    # 全局默认过期时间:10分钟 (单位: 毫秒)
      key-prefix: "myapp:"    # 全局 Key 前缀,防止多个项目共用同一个 Redis 时 Key 冲突
      use-key-prefix: true

第三步:自定义 Redis 缓存配置类(🌟 核心,解决乱码)

为什么需要这个配置类?

  1. 将 Key 序列化为 String(人类可读)。
  2. 将 Value 序列化为 JSON(人类可读,且跨语言)。
  3. 解决 Java 8 LocalDateTime 等时间类型的 JSON 序列化报错问题。
  4. 为不同的 cacheName (业务模块) 设置不同的过期时间。
复制代码
复制代码
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 com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
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.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;
import java.util.HashMap;
import java.util.Map;

@Configuration
@EnableCaching // 开启缓存
public class RedisCacheConfig {

    @Bean
    public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
        // 1. 配置 JSON 序列化器 (解决 LocalDateTime 等时间类型序列化问题)
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
        objectMapper.registerModule(new JavaTimeModule()); // 支持 Java 8 时间 API
        
        GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer(objectMapper);
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();

        // 2. 配置默认的 RedisCacheConfiguration
        RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofMinutes(30)) // 默认 30 分钟过期
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(stringRedisSerializer))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jsonRedisSerializer))
                .disableCachingNullValues(); // 不缓存 null 值 (防止缓存穿透的简单策略,视业务而定)

        // 3. 针对不同的 cacheName 设置不同的过期时间 (精细化控制)
        Map<String, RedisCacheConfiguration> cacheConfigurations = new HashMap<>();
        // 用户信息缓存 1 小时
        cacheConfigurations.put("users", defaultConfig.entryTtl(Duration.ofHours(1)));
        // 订单信息缓存 5 分钟
        cacheConfigurations.put("orders", defaultConfig.entryTtl(Duration.ofMinutes(5)));
        // 字典/配置类数据缓存 1 天
        cacheConfigurations.put("dicts", defaultConfig.entryTtl(Duration.ofDays(1)));

        // 4. 构建 RedisCacheManager
        return RedisCacheManager.builder(connectionFactory)
                .cacheDefaults(defaultConfig) // 应用默认配置
                .withInitialCacheConfigurations(cacheConfigurations) // 应用自定义配置
                .transactionAware() // 开启事务感知 (在事务提交后才删除/更新缓存)
                .build();
    }
}

第四步:在 Service 层使用缓存

配置完成后,在业务代码中的使用方式与本地缓存完全一致,但数据实际存储在 Redis 中。

复制代码
复制代码
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    // 1. 查询缓存:如果 Redis 中有,直接返回;没有则查 DB 并放入 Redis
    // value="users" 对应上面配置类中设置的 1 小时过期时间
    // 最终在 Redis 中的 Key 为: myapp:users::1001
    @Cacheable(value = "users", key = "#id")
    public User getUserById(Long id) {
        return userMapper.selectById(id);
    }

    // 2. 更新缓存:执行方法,并将返回值更新到 Redis 中
    @CachePut(value = "users", key = "#user.id")
    public User updateUser(User user) {
        userMapper.updateById(user);
        return user;
    }

    // 3. 删除缓存:执行方法,并删除 Redis 中对应的 Key
    @CacheEvict(value = "users", key = "#id")
    public void deleteUser(Long id) {
        userMapper.deleteById(id);
    }
    
    // 4. 批量清除:清除 "users" 命名空间下的所有缓存
    @CacheEvict(value = "users", allEntries = true)
    public void clearAllUserCache() {
        // ...
    }
}

💡 企业级避坑与进阶指南

1. Redis 中的 Key 长什么样?

经过上述配置,存入 Redis 的数据会非常清晰:

  • Key : myapp:users::1001 (前缀 + cacheName + :: + 方法参数)

  • Value :

    复制代码
    复制代码
    {
      "@class": "com.example.entity.User",
      "id": 1001,
      "username": "张三",
      "createTime": [2023, 10, 25, 10, 30, 0]
    }

    (注:@class 是 Jackson 序列化留下的类型标识,用于反序列化时还原正确的 Java 对象,这是正常且必须的。)

2. 缓存穿透问题(查询数据库根本不存在的数据)

上面的配置中我写了 .disableCachingNullValues()(不缓存 null)。

  • 优点:防止把无效数据存入 Redis 浪费内存。
  • 缺点 :如果黑客恶意疯狂请求一个不存在的 ID(如 id = -1),每次都会穿透到 MySQL,导致数据库宕机(缓存穿透)。
  • 解决方案
    • 方案 A:在代码层拦截(如使用 Guava RateLimiter 限流,或校验 ID 格式)。
    • 方案 B :允许缓存 null,但给 null 值设置极短的过期时间。修改配置类:去掉 .disableCachingNullValues(),并配合自定义的 CacheResolver 或在业务代码里手动返回一个空对象。
3. 复杂 Key 的生成 (自定义 KeyGenerator)

如果方法有多个参数,默认的 key = "#p0" 写法容易出错。可以自定义一个全局的 Key 生成器:

复制代码
复制代码
@Bean("myKeyGenerator")
public KeyGenerator myKeyGenerator() {
    return (target, method, params) -> {
        StringBuilder sb = new StringBuilder();
        sb.append(target.getClass().getSimpleName()).append(":");
        sb.append(method.getName()).append(":");
        for (Object param : params) {
            sb.append(param.toString()).append("_");
        }
        return sb.toString().replaceAll(":$", ""); // 去掉最后的冒号
    };
}

使用方式

复制代码
复制代码
@Cacheable(value = "users", keyGenerator = "myKeyGenerator")
public List<User> queryUsers(String name, Integer age) { ... }
4. 多级缓存 (L1 Caffeine + L2 Redis)

对于极高并发的读场景(如秒杀、首页热点数据),单次请求 Redis 也会有网络开销。 可以使用 jetcachecaffeine-redis 等开源组件实现两级缓存

  • 一级缓存 (Caffeine):存在应用本地内存,抗住 90% 的极致并发,过期时间设为 10 秒。
  • 二级缓存 (Redis) :存在分布式集群,抗住 10% 的漏网请求,过期时间设为 10 分钟。 (注:这需要引入额外的第三方 Starter,原生 Spring Cache 不直接支持多级缓存。)
相关推荐
隔窗听雨眠43 分钟前
缓存增强生成CAG:用预加载KV缓存突破RAG实时检索瓶颈
缓存
chuan.bai1 小时前
Java RAG 实战(第 8 篇):Spring Boot RAG 查询 API
java·spring boot·贪心算法
vx-程序开发1 小时前
springboot旅游推介平台---附源码24175
java·spring boot·python·spring cloud·eclipse·django·idea
小蒜学长2 小时前
“喵汪联盟”宠物领养系统的设计与实现(代码+数据库+LW)
java·spring boot·后端·宠物
光影少年2 小时前
react离线缓存、图片缓存方案
开发语言·前端·javascript·react native·react.js·缓存·前端框架
xbgRS5 小时前
springboot的自动装配
java·spring boot
嘻哈∠※12 小时前
0061基于 SpringBoot 的投稿与稿件处理系统设计与实现
java·spring boot·后端
m0_3807438712 小时前
给大模型调用加一层本地缓存,让重复请求直接命中磁盘
缓存
FakeOccupational14 小时前
【电路笔记 STM32】Cortex-M7 内核上的数据缓存(D-Cache)结构+MPU+DMA&Cache+STM32CubeMX配置
笔记·stm32·缓存