在 Spring Boot + MyBatis 架构中,所谓的"程序级别缓存"(应用层缓存)通常有两种实现方式:
- MyBatis 自带的二级缓存(Mapper 级别,作用于数据访问层)。
- Spring Cache 缓存抽象 (Service 级别,结合 Redis/Caffeine 等,企业级开发最推荐)。
(注:MySQL 8.0 已经彻底移除了数据库自身的 Query Cache,因此将缓存前置到程序层是标准做法。)
下面为您详细介绍这两种方案的开启和配置方法。
方案一:开启 MyBatis 二级缓存(Mapper 级别)
MyBatis 一级缓存(SqlSession 级别)是默认开启的。二级缓存是跨 SqlSession 的,作用于同一个 Mapper 的 namespace。
1. 开启全局配置
在 application.yml 或 application.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 的。如果
UserMapper和OrderMapper都关联了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 即可) |
| 生产环境推荐度 | ⭐ (仅适合单表、不常更新的字典表) | ⭐⭐⭐⭐⭐ (企业级标准做法) |
最佳实践建议:
- 在
application.yml中设置mybatis.configuration.cache-enabled: false关闭 MyBatis 二级缓存。 - 引入
Spring Cache,如果是单机应用使用 Caffeine ,如果是微服务/集群应用使用 Redis。 - 把缓存逻辑写在 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 缓存配置类(🌟 核心,解决乱码)
为什么需要这个配置类?
- 将 Key 序列化为
String(人类可读)。 - 将 Value 序列化为
JSON(人类可读,且跨语言)。 - 解决 Java 8
LocalDateTime等时间类型的 JSON 序列化报错问题。 - 为不同的
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 也会有网络开销。 可以使用 jetcache 或 caffeine-redis 等开源组件实现两级缓存:
- 一级缓存 (Caffeine):存在应用本地内存,抗住 90% 的极致并发,过期时间设为 10 秒。
- 二级缓存 (Redis) :存在分布式集群,抗住 10% 的漏网请求,过期时间设为 10 分钟。 (注:这需要引入额外的第三方 Starter,原生 Spring Cache 不直接支持多级缓存。)