Spring Cache技术使用方案详解
本文深入探讨Spring Cache技术,从缓存基础概念出发,逐步深入到核心原理、最佳实践及常见问题解决方案。
一、缓存概述
1.1 为什么需要缓存
数据库往往成为系统性能瓶颈,主要原因:
| 问题 | 表现 | 影响 |
|---|---|---|
| 磁盘I/O瓶颈 | 数据库查询涉及磁盘读取 | 响应时间长 |
| 连接资源有限 | 数据库连接数有限 | 高并发时请求排队 |
| CPU资源消耗 | 复杂SQL计算消耗CPU | 数据库负载高 |
缓存的本质:用空间换时间,将频繁访问的数据存储在更快的存储介质中。
1.2 缓存应用场景
适合缓存: 读多写少、一致性要求不严格、计算成本高、热点数据集中
典型场景: 商品信息、用户信息、字典数据、热点内容、会话信息
不适合缓存: 写多读少、强一致性要求、数据量大且冷门、敏感数据
1.3 缓存优缺点
| 优点 | 缺点 |
|---|---|
| 性能提升、减轻数据库压力 | 数据一致性挑战 |
| 提升吞吐量、降低网络开销 | 额外维护成本、内存成本 |
二、缓存实现方案
2.1 传统实现方式
传统Redis API实现示例:
@Service
public class UserServiceImpl {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private UserMapper userMapper;
public User getUserById(Long id) {
String cacheKey = "user:" + id;
User user = (User) redisTemplate.opsForValue().get(cacheKey);
if (user != null) return user;
user = userMapper.selectById(id);
if (user != null) {
redisTemplate.opsForValue().set(cacheKey, user, 24, TimeUnit.HOURS);
}
return user;
}
public void updateUser(User user) {
userMapper.updateById(user);
redisTemplate.delete("user:" + user.getId());
}
}
传统方式的痛点:
- 代码侵入性强(业务与缓存耦合)
- 代码重复(相似逻辑多处重复)
- 维护困难(修改策略需改多处)
- 缺乏统一标准(命名风格不一致)
- 功能受限(难以实现条件缓存、多级缓存)
2.2 缓存更新策略
| 模式 | 说明 | 适用场景 |
|---|---|---|
| Cache Aside | 读时先查缓存,未命中则查数据库并写入缓存;写时先更新数据库再删除缓存 | 最常用,适合大多数场景 |
| Read/Write Through | 应用只与缓存交互,缓存层负责与数据库同步 | 对一致性要求较高 |
| Write Behind | 写操作只更新缓存,数据库异步批量更新 | 写入频繁、一致性要求不高 |
Cache Aside模式流程:
读取:应用 → 缓存(命中返回) → 未命中 → 数据库 → 写入缓存 → 返回
更新:应用 → 数据库 → 删除缓存
为什么删除缓存而非更新?
- 避免并发写问题(两线程同时更新可能顺序不一致)
- 资源优化(数据修改但不常读取时,更新缓存是浪费)
- 实现简单(删除操作更轻量)
2.3 缓存删除策略
| 策略 | 原理 | 适用场景 |
|---|---|---|
| 直接删除 | 数据变更时立即删除缓存 | 一般场景,一致性较好 |
| 延迟双删 | 删除缓存 → 更新数据库 → 延迟后再次删除 | 高并发读取、数据库有主从延迟 |
| 异步删除(MQ) | 通过消息队列异步处理缓存删除 | 需要解耦、可靠性要求高 |
延迟双删实现:
@Transactional
public void updateUser(User user) {
String cacheKey = "user:" + user.getId();
redisTemplate.delete(cacheKey); // 第一次删除
userMapper.updateById(user); // 更新数据库
CompletableFuture.runAsync(() -> {
Thread.sleep(500); // 延迟(大于主从同步时间)
redisTemplate.delete(cacheKey); // 第二次删除
});
}
三、缓存三大经典问题
3.1 缓存穿透
问题: 查询根本不存在的数据,请求每次都穿透到数据库。
解决方案:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 缓存空对象 | 实现简单 | 占用内存、需清理 | 查询Key固定 |
| 布隆过滤器 | 内存小、效率高 | 有误判率、需预热 | 数据量大 |
布隆过滤器实现:
@PostConstruct
public void init() {
RBloomFilter<Long> bloomFilter = redissonClient.getBloomFilter("user:bloom");
bloomFilter.tryInit(1000000, 0.0001); // 100万元素,0.01%误判率
userMapper.selectAllIds().forEach(bloomFilter::add);
}
public User getUserById(Long id) {
if (!bloomFilter.contains(id)) return null; // 确定不存在
// ... 查询缓存和数据库
}
3.2 缓存击穿
问题: 热点Key过期瞬间,大量并发请求穿透到数据库。
解决方案:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 互斥锁 | 数据强一致 | 并发等待 | 一致性要求高 |
| 逻辑过期 | 无等待、高可用 | 短暂不一致 | 可用性优先 |
互斥锁实现(Redisson):
public User getUserWithLock(Long id) {
String key = "user:" + id;
User user = (User) redisTemplate.opsForValue().get(key);
if (user != null) return user;
RLock lock = redissonClient.getLock("lock:user:" + id);
try {
if (lock.tryLock(10, TimeUnit.SECONDS)) {
user = (User) redisTemplate.opsForValue().get(key); // 双重检查
if (user != null) return user;
user = userMapper.selectById(id);
if (user != null) {
redisTemplate.opsForValue().set(key, user, 1, TimeUnit.HOURS);
}
return user;
}
Thread.sleep(50); // 获取锁失败,短暂等待后重试
return getUserWithLock(id);
} finally {
if (lock.isHeldByCurrentThread()) lock.unlock();
}
}
逻辑过期实现:
// 缓存数据结构(包含逻辑过期时间)
@Data
public class CacheData<T> {
private T data;
private LocalDateTime expireTime;
}
public User getUserWithLogicalExpire(Long id) {
String key = "user:" + id;
CacheData<User> cacheData = (CacheData<User>) redisTemplate.opsForValue().get(key);
if (cacheData == null) {
// 缓存不存在,需要重建(加锁)
return rebuildCacheWithLock(id);
}
if (cacheData.getExpireTime().isAfter(LocalDateTime.now())) {
return cacheData.getData(); // 未过期,直接返回
}
// 已过期,异步重建,返回旧数据
CompletableFuture.runAsync(() -> rebuildCacheWithLock(id));
return cacheData.getData();
}
3.3 缓存雪崩
问题: 大量Key同时过期或缓存服务宕机,请求全部打到数据库。
解决方案:
| 方案 | 说明 |
|---|---|
| 过期时间加随机值 | 避免同时过期 |
| 缓存高可用架构 | 主从+哨兵、多级缓存 |
| 限流降级 | 保护数据库 |
| 多级缓存 | 本地缓存(L1) + 分布式缓存(L2) |
多级缓存架构:
请求 → Caffeine(L1) → Redis(L2) → Database
(毫秒级) (亚毫秒级) (瓶颈点)
四、Spring Cache技术介绍
4.1 Spring Cache是什么
Spring Cache是Spring框架提供的缓存抽象层,通过注解将缓存逻辑从业务代码分离。
┌─────────────────────────────────────────────────┐
│ Spring Cache 架构 │
├─────────────────────────────────────────────────┤
│ 应用层:@Cacheable/@CachePut/@CacheEvict │
│ ↓ │
│ 抽象层:CacheManager/Cache/CacheResolver │
│ ↓ │
│ 实现层:Redis/Caffeine/Ehcache │
└─────────────────────────────────────────────────┘
核心特性: 声明式缓存、抽象化、多种缓存支持、SpEL支持、条件缓存
4.2 Spring Cache的优势
| 维度 | 传统方式 | Spring Cache |
|---|---|---|
| 代码量 | 每方法手动处理 | 一行注解 |
| 可读性 | 代码混杂 | 业务逻辑清晰 |
| 维护性 | 改多处代码 | 修改注解即可 |
| 灵活性 | 大量改动 | 更换配置 |
4.3 Cache Aside模式的注解实现
重要说明: Spring Cache注解底层采用的是Cache Aside模式,这与第二章介绍的传统手动实现方式逻辑完全一致,但代码更加简洁优雅。
Cache Aside模式对比:
| 操作 | 手动实现 | Spring Cache注解 |
|---|---|---|
| 读取 | 先查缓存→未命中查DB→写入缓存 | @Cacheable |
| 更新 | 更新DB→删除缓存 | @CacheEvict 或 @CachePut |
| 删除 | 删除DB→删除缓存 | @CacheEvict |
@Cacheable的Cache Aside流程:
┌──────────────────────────────────────────────────────────┐
│ @Cacheable 执行流程 │
├──────────────────────────────────────────────────────────┤
│ 1. 方法调用 → 检查缓存 │
│ ↓ │
│ 2. 缓存命中? ──是──→ 直接返回缓存数据 │
│ │ │
│ 否 │
│ ↓ │
│ 3. 执行方法体(查询数据库) │
│ ↓ │
│ 4. 将结果写入缓存 │
│ ↓ │
│ 5. 返回结果 │
└──────────────────────────────────────────────────────────┘
代码对比及效果等价示例:
// ========== 手动实现(Cache Aside模式) ==========
public User getUserById(Long id) {
String key = "users::" + id;
User user = (User) redisTemplate.opsForValue().get(key); // 1. 查缓存
if (user != null) {
return user; // 2. 命中返回
}
user = userMapper.selectById(id); // 3. 查数据库
if (user != null) {
redisTemplate.opsForValue().set(key, user); // 4. 写入缓存
}
return user;
}
// ========== Spring Cache注解(效果完全相同) ==========
@Cacheable(value = "users", key = "#id")
public User getUserById(Long id) {
return userMapper.selectById(id); // 注解自动处理缓存逻辑
}
核心结论: Spring Cache注解本质上是对Cache Aside模式的封装,开发者只需关注业务逻辑,缓存策略由框架自动处理,既保证了缓存模式的正确性,又大大简化了代码。
五、Spring Cache核心注解
5.1 @Cacheable
作用: 查询缓存,有则返回,无则执行方法并缓存结果。
| 属性 | 说明 | 示例 |
|---|---|---|
value/cacheNames |
缓存区域名称 | "users" |
key |
缓存Key(SpEL) | "#id" |
condition |
条件判断(SpEL) | "#id > 0" |
unless |
排除条件(SpEL) | "#result == null" |
sync |
同步模式(防击穿) | true |
// 基础用法
@Cacheable(value = "users", key = "#id")
public User getUserById(Long id) { return userMapper.selectById(id); }
// 条件缓存
@Cacheable(value = "users", key = "#id", condition = "#id > 0", unless = "#result == null")
public User getUserById(Long id) { return userMapper.selectById(id); }
// 防击穿(sync=true)
@Cacheable(value = "users", key = "#id", sync = true)
public User getUserById(Long id) { return userMapper.selectById(id); }
5.2 @CachePut
作用: 执行方法并将结果放入缓存(不检查缓存是否存在)。
@CachePut(value = "users", key = "#user.id")
public User updateUser(User user) {
userMapper.updateById(user);
return user;
}
5.3 @CacheEvict
作用: 删除缓存。
| 属性 | 说明 | 默认值 |
|---|---|---|
allEntries |
删除整个缓存区域 | false |
beforeInvocation |
方法执行前删除 | false |
// 删除单个缓存
@CacheEvict(value = "users", key = "#id")
public void deleteUser(Long id) { userMapper.deleteById(id); }
// 删除整个缓存区域
@CacheEvict(value = "users", allEntries = true)
public void clearAllUsers() { }
// 方法执行前删除(防止异常导致缓存未清除)
@CacheEvict(value = "users", key = "#id", beforeInvocation = true)
public void deleteUser(Long id) { userMapper.deleteById(id); }
5.4 @Caching
作用: 组合多个缓存操作。
@Caching(evict = {
@CacheEvict(value = "users", key = "#user.id"),
@CacheEvict(value = "userList", allEntries = true)
})
public void updateUser(User user) {
userMapper.updateById(user);
}
5.5 @CacheConfig
作用: 类级别缓存配置,避免重复配置。
@Service
@CacheConfig(cacheNames = "users")
public class UserServiceImpl {
@Cacheable(key = "#id") // 继承类级别cacheNames
public User getUserById(Long id) { return userMapper.selectById(id); }
}
六、缓存管理器与解析器
6.1 CacheManager
常用实现:RedisCacheManager、CaffeineCacheManager、EhcacheCacheManager
Redis缓存管理器配置:
@Configuration
@EnableCaching
public class RedisCacheConfig {
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer();
RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(serializer))
.entryTtl(Duration.ofMinutes(30))
.computePrefixWith(cacheName -> "app:" + cacheName + ":")
.disableCachingNullValues();
// 不同缓存区域个性化配置
Map<String, RedisCacheConfiguration> configMap = Map.of(
"users", defaultConfig.entryTtl(Duration.ofHours(1)),
"dictionaries", defaultConfig.entryTtl(Duration.ofHours(24)),
"captcha", defaultConfig.entryTtl(Duration.ofMinutes(5))
);
return RedisCacheManager.builder(factory)
.cacheDefaults(defaultConfig)
.withInitialCacheConfigurations(configMap)
.transactionAware()
.build();
}
}
6.2 自定义KeyGenerator
@Component
public class CustomKeyGenerator implements KeyGenerator {
@Override
public Object generate(Object target, Method method, Object... params) {
return target.getClass().getSimpleName() + ":"
+ method.getName() + ":"
+ Arrays.toString(params);
}
}
七、Spring Expression Language (SpEL)
7.1 SpEL简介
SpEL是什么
Spring Expression Language(SpEL)是Spring框架提供的一种强大的表达式语言,用于在运行时查询和操作对象图。它类似于EL表达式(JSP中使用),但功能更强大,支持方法调用、属性访问、运算符、集合操作等。
语法特点
- 使用
#{...}作为表达式定界符 - 支持字符串、数字、布尔值等基本类型
- 支持对象属性访问、方法调用
- 支持算术、关系、逻辑运算
- 支持正则匹配、集合投影、选择等高级操作
应用领域
| 领域 | 应用场景 | 示例 |
|---|---|---|
| Spring Cache | 缓存Key生成、条件判断 | @Cacheable(key = "#user.id") |
| Spring Security | 权限表达式 | @PreAuthorize("hasRole('ADMIN')") |
| Spring Config | 配置属性引用 | @Value("#{systemProperties['app.name']}") |
| Spring AOP | 切点表达式条件 | @Pointcut("execution(* com.*.*(..))") |
| Thymeleaf | 模板变量渲染 | <p th:text="${user.name}"> |
| Spring Data | 查询条件动态绑定 | @Query("select u from User u where u.age > :#{#age}") |
7.2 SpEL上下文对象详解
root对象说明
在SpEL中,#root是一个特殊的内置对象,代表当前表达式的根上下文对象。Spring Cache在解析SpEL表达式时,会创建一个CacheExpressionRootObject作为root对象,包含方法、参数、目标对象等信息。
┌─────────────────────────────────────────────────────┐
│ #root 对象结构 │
├─────────────────────────────────────────────────────┤
│ #root.target → 被调用的目标对象(Service实例) │
│ #root.targetClass → 目标对象的Class │
│ #root.method → 当前调用的Method对象 │
│ #root.methodName → 方法名称 │
│ #root.args → 方法参数数组 │
│ #root.caches → 缓存集合 │
│ #root.result → 方法返回值(@CachePut可用) │
└─────────────────────────────────────────────────────┘
上下文对象一览
| 对象 | 说明 | 示例 |
|---|---|---|
#root.method |
当前调用的Method对象 | #root.method.name → "getUserById" |
#root.methodName |
方法名称字符串 | #root.methodName → "getUserById" |
#root.target |
目标对象(被调用的Service) | #root.target.class → UserServiceImpl |
#root.targetClass |
目标对象的Class | #root.targetClass.simpleName |
#root.args |
方法参数数组 | #root.args[0] → 第一个参数 |
#root.caches |
当前操作涉及的缓存集合 | #root.caches[0].name |
#root.result |
方法返回值(仅@CachePut/@Cacheable的unless可用) | #root.result.id |
#参数名 |
直接引用方法参数 | #id、#user.name |
#p0、#a0 |
按位置索引引用参数 | #p0 → 第一个参数 |
#meta |
元数据信息 | #meta.methodName |
使用示例
// 使用方法名作为Key的一部分
@Cacheable(key = "#root.methodName + ':' + #id")
public User getUserById(Long id) { ... } // Key: "getUserById:123"
// 使用目标类名和方法名组合
@Cacheable(key = "#root.targetClass.simpleName + ':' + #root.methodName + ':' + #id")
public User getUserById(Long id) { ... } // Key: "UserServiceImpl:getUserById:123"
// 动态获取参数(适用于参数名不确定场景)
@Cacheable(key = "'user:' + #root.args[0]")
public User getUserById(Long id) { ... }
// 条件判断:根据方法名决定是否缓存
@Cacheable(condition = "#root.methodName.startsWith('get')")
public User getUserById(Long id) { ... }
7.3 常用表达式示例
// 基本参数
@Cacheable(key = "#id")
// 对象属性
@Cacheable(key = "#user.id")
// 字符串拼接
@Cacheable(key = "'user:' + #userId + ':order:' + #orderId")
// 条件运算
@Cacheable(key = "#id != null ? #id : 'default'")
// 条件判断
@Cacheable(condition = "#id > 0 && #status == 'ACTIVE'")
// 正则匹配
@Cacheable(condition = "#email matches '^[A-Za-z0-9+_.-]+@(.+)$'")
// 排除null
@Cacheable(unless = "#result == null")
7.4 SpEL运算符速查
| 类型 | 运算符 |
|---|---|
| 算术 | +, -, *, /, %, ^ |
| 关系 | ==, !=, <, >, <=, >= |
| 逻辑 | &&, ||, ! |
| 条件 | ?: (三元)、?: (Elvis) |
| 正则 | matches |
| 类型 | T() |
八、命名规范与最佳实践
8.1 缓存Key命名规范
格式: [应用名]:[业务模块]:[业务含义]:[唯一标识]
示例:
order:trade:order:123456 订单ID
user:auth:token:abc123 用户token
system:dict:province:all 省份字典
8.2 过期时间策略
| 数据变化频率 | 过期时间 |
|---|---|
| 极少变化(字典、配置) | 24小时或更长 |
| 偶尔变化(用户信息) | 1-6小时 |
| 经常变化(商品信息) | 5-30分钟 |
| 高频变化(库存、价格) | 不缓存或秒级 |
| 实时数据(验证码) | 分钟级 |
8.3 最佳实践
| 实践 | 说明 |
|---|---|
| 防穿透 | 缓存空对象或布隆过滤器 |
| 防击穿 | sync=true 或互斥锁 |
| 防雪崩 | 过期时间加随机值、多级缓存 |
| Key规范 | 使用常量类统一管理 |
| 条件缓存 | condition/unless 灵活控制 |
九、项目实战示例
9.1 Maven依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
9.2 字典服务缓存
@Service
@CacheConfig(cacheNames = "dictionaries")
public class DictionaryServiceImpl {
@Autowired
private DictionaryMapper dictionaryMapper;
// 查询字典树(缓存)
@Cacheable(key = "'all'", sync = true)
public List<Dictionary> getDictionaryTree() {
return buildTree(dictionaryMapper.selectAll());
}
// 新增字典(清除缓存)
@CacheEvict(allEntries = true)
public void addDictionary(Dictionary dict) {
dictionaryMapper.insert(dict);
}
// 更新字典(清除缓存)
@CacheEvict(allEntries = true)
public void updateDictionary(Dictionary dict) {
dictionaryMapper.updateById(dict);
}
}
最佳实践:使用
@CacheConfig在类级别统一配置cacheNames,方法上只需配置key等差异化属性,代码更简洁。
9.3 用户服务缓存
@Service
@CacheConfig(cacheNames = "users")
public class UserServiceImpl {
@Autowired
private UserMapper userMapper;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Cacheable(key = "#id", unless = "#result == null")
public User getById(Long id) { return userMapper.selectById(id); }
@CachePut(key = "#user.id")
public User update(User user) {
userMapper.updateById(user);
return userMapper.selectById(user.getId());
}
// 删除用户:@CacheEvict清除主缓存(users:id),辅助缓存(users:username:xxx)需手动清除
@CacheEvict(key = "#id")
public void delete(Long id) {
User user = userMapper.selectById(id);
if (user != null) {
redisTemplate.delete("users:username:" + user.getUsername()); // 手动清除按username索引的辅助缓存
userMapper.deleteById(id);
}
}
}
十、分布式环境高级问题
10.1 缓存一致性保障
问题: 缓存与数据库数据不一致
解决方案:
| 方案 | 说明 |
|---|---|
| Cache-Aside + 重试 | 更新数据库后删除缓存,失败时重试或发MQ |
| 延迟双删 | 解决并发读写导致脏数据 |
10.2 序列化问题
问题: 类型信息丢失、日期时间格式、泛型集合反序列化
解决方案: 使用Jackson配置类型信息保留
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
mapper.activateDefaultTyping(BasicPolymorphicTypeValidator.builder()
.allowIfBaseType(Object.class).build(), ObjectMapper.DefaultTyping.NON_FINAL);
10.3 分布式缓存同步
问题: 多实例本地缓存无法及时同步
解决方案: Redis Pub/Sub通知各实例清除本地缓存
实现示例:
// 发布缓存清除消息
@Service
public class CacheSyncPublisher {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final String CACHE_INVALIDATE_CHANNEL = "cache:invalidate";
public void publishInvalidation(String cacheName, String key) {
Map<String, String> message = Map.of("cacheName", cacheName, "key", key);
redisTemplate.convertAndSend(CACHE_INVALIDATE_CHANNEL, JSON.toJSONString(message));
}
}
// 监听缓存清除消息
@Component
public class CacheSyncListener {
@Autowired
private CacheManager cacheManager;
@RedisListener(channel = "cache:invalidate")
public void onMessage(String message) {
Map<String, String> data = JSON.parseObject(message, Map.class);
Cache cache = cacheManager.getCache(data.get("cacheName"));
if (cache != null) {
cache.evict(data.get("key")); // 清除本地缓存
}
}
}
十一、总结
核心要点回顾
| 类别 | 内容 |
|---|---|
| 注解 | @Cacheable(查询)、@CachePut(更新)、@CacheEvict(删除)、@Caching(组合)、@CacheConfig(类配置) |
| 属性 | value/key/condition/unless/sync/allEntries/beforeInvocation |
| 防范 | 穿透(空对象/布隆)、击穿(sync/锁)、雪崩(随机TTL/多级缓存) |
适用场景建议
| 场景 | 推荐 |
|---|---|
| 读多写少 | @Cacheable |
| 写后读 | @CachePut |
| 写后删 | @CacheEvict(推荐) |
| 热点数据 | sync=true |
| 高可用 | 多级缓存 |
| 分布式环境 | Pub/Sub同步 |

浙公网安备 33010602011771号