第 44 课:Spring Boot缓存
本课目标:掌握Spring Cache抽象层,集成Redis实现缓存,理解缓存穿透/击穿/雪崩问题及解决方案。
一、概念讲解
1.1 为什么需要缓存
无缓存流程:
数据库 ──> 100ms ──> 返回结果
每次请求都访问数据库,压力大
有缓存流程:
缓存(1ms) ──> 命中 ──> 返回结果
缓存(1ms) ──> 未命中 ──> 数据库(100ms) ──> 写入缓存 ──> 返回结果
1.2 Spring Cache注解体系
┌─────────────────────────────────────────────────┐
│ Spring Cache 核心注解 │
├─────────────────────────────────────────────────┤
│ @Cacheable 查询:先查缓存,有则返回,无则查库 │
│ @CachePut 更新:每次执行方法,更新缓存 │
│ @CacheEvict 删除:清除指定缓存 │
│ @Caching 组合:同时操作多个缓存 │
│ @CacheConfig 类级别:统一配置缓存名称等 │
└─────────────────────────────────────────────────┘
1.3 缓存常见问题
缓存穿透(查不存在的数据):
请求 ──> 缓存(无) ──> 数据库(无) ──> 返回null
→ 恶意请求大量查询不存在的ID,绕过缓存直接打数据库
→ 解决:缓存空值 + 布隆过滤器
缓存击穿(热key过期):
缓存过期 ──> 大量请求同时查库 ──> 数据库压力骤增
→ 解决:互斥锁(只放一个请求查库)+ 永不过期
缓存雪崩(大量key同时过期):
大量缓存同时过期 ──> 所有请求都打到数据库
→ 解决:随机过期时间 + 集群部署 + 熔断降级
1.4 Redis数据结构
String: key → value (最常用)
Hash: key → field → value (对象存储)
List: key → [value1, value2, ...] (队列)
Set: key → {value1, value2, ...} (集合)
ZSet: key → score → value (排行榜)
二、语法格式
2.1 @Cacheable用法
// 基本用法
@Cacheable(value = "users", key = "#id")
public User findById(Long id) { ... }
// 条件缓存
@Cacheable(value = "users", key = "#id", condition = "#id > 0")
public User findById(Long id) { ... }
// 不缓存null值
@Cacheable(value = "users", key = "#id", unless = "#result == null")
public User findById(Long id) { ... }
2.2 @CacheEvict用法
// 删除单个缓存
@CacheEvict(value = "users", key = "#id")
public void delete(Long id) { ... }
// 清空整个缓存
@CacheEvict(value = "users", allEntries = true)
public void clearCache() { ... }
三、代码案例
3.1 添加依赖
<!-- Spring Cache -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
3.2 Redis配置
# application.yml
spring:
redis:
host: localhost
port: 6379
password:
database: 0
timeout: 5000ms
lettuce:
pool:
max-active: 20
max-idle: 10
min-idle: 5
max-wait: 3000ms
3.3 Redis配置类
package com.example.demo.config;
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.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.core.RedisTemplate;
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;
@Configuration
@EnableCaching
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(
RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// JSON序列化
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.activateDefaultTyping(
LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL);
GenericJackson2JsonRedisSerializer jsonSerializer =
new GenericJackson2JsonRedisSerializer(om);
StringRedisSerializer stringSerializer = new StringRedisSerializer();
template.setKeySerializer(stringSerializer);
template.setHashKeySerializer(stringSerializer);
template.setValueSerializer(jsonSerializer);
template.setHashValueSerializer(jsonSerializer);
template.afterPropertiesSet();
return template;
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
// JSON序列化配置
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.activateDefaultTyping(
LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL);
GenericJackson2JsonRedisSerializer jsonSerializer =
new GenericJackson2JsonRedisSerializer(om);
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30)) // 默认过期30分钟
.serializeKeysWith(
RedisSerializationContext.SerializationPair
.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(
RedisSerializationContext.SerializationPair
.fromSerializer(jsonSerializer))
.disableCachingNullValues(); // 不缓存null
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
}
}
3.4 带缓存的Service
package com.example.demo.service;
import com.example.demo.entity.User;
import com.example.demo.repository.UserRepository;
import org.springframework.cache.annotation.*;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@CacheConfig(cacheNames = "users")
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
// 查询:先查缓存,有则返回,无则查库并缓存
@Cacheable(key = "#id", unless = "#result == null")
public User findById(Long id) {
simulateSlowService();
return userRepository.findById(id).orElse(null);
}
// 查询所有:缓存整个列表
@Cacheable(key = "'all'")
public List<User> findAll() {
simulateSlowService();
return userRepository.findAll();
}
// 更新:每次执行方法,更新缓存
@CachePut(key = "#user.id")
public User update(User user) {
User existing = userRepository.findById(user.getId())
.orElseThrow(() -> new RuntimeException("用户不存在"));
existing.setUsername(user.getUsername());
existing.setEmail(user.getEmail());
existing.setAge(user.getAge());
return userRepository.save(existing);
}
// 删除:清除指定缓存
@CacheEvict(key = "#id")
public void deleteById(Long id) {
userRepository.deleteById(id);
}
// 清空所有缓存
@CacheEvict(allEntries = true)
public void clearAllCache() {
System.out.println("缓存已清空");
}
// 组合操作
@Caching(
put = { @CachePut(key = "#user.id") },
evict = { @CacheEvict(key = "'all'") }
)
public User save(User user) {
return userRepository.save(user);
}
// 模拟慢查询(测试用)
private void simulateSlowService() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
3.5 缓存Controller
package com.example.demo.controller;
import com.example.demo.common.Result;
import com.example.demo.entity.User;
import com.example.demo.service.UserService;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/{id}")
public Result<User> getById(@PathVariable Long id) {
long start = System.currentTimeMillis();
User user = userService.findById(id);
long cost = System.currentTimeMillis() - start;
System.out.println("查询耗时: " + cost + "ms");
return Result.success(user);
}
@GetMapping
public Result<List<User>> list() {
long start = System.currentTimeMillis();
List<User> users = userService.findAll();
long cost = System.currentTimeMillis() - start;
System.out.println("查询耗时: " + cost + "ms");
return Result.success(users);
}
@PutMapping("/{id}")
public Result<User> update(@PathVariable Long id, @RequestBody User user) {
user.setId(id);
return Result.success(userService.update(user));
}
@DeleteMapping("/{id}")
public Result<Void> delete(@PathVariable Long id) {
userService.deleteById(id);
return Result.success("删除成功", null);
}
@PostMapping("/clear-cache")
public Result<Void> clearCache() {
userService.clearAllCache();
return Result.success("缓存已清空", null);
}
}
3.6 手动操作Redis(RedisTemplate)
package com.example.demo.service;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
public class RedisService {
private final RedisTemplate<String, Object> redisTemplate;
public RedisService(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
// 设置值
public void set(String key, Object value) {
redisTemplate.opsForValue().set(key, value);
}
// 设置值(带过期时间)
public void set(String key, Object value, long timeout, TimeUnit unit) {
redisTemplate.opsForValue().set(key, value, timeout, unit);
}
// 获取值
public Object get(String key) {
return redisTemplate.opsForValue().get(key);
}
// 删除key
public Boolean delete(String key) {
return redisTemplate.delete(key);
}
// 判断key是否存在
public Boolean hasKey(String key) {
return redisTemplate.hasKey(key);
}
// 设置过期时间
public Boolean expire(String key, long timeout, TimeUnit unit) {
return redisTemplate.expire(key, timeout, unit);
}
// 自增
public Long increment(String key) {
return redisTemplate.opsForValue().increment(key);
}
// Hash操作
public void hSet(String key, String field, Object value) {
redisTemplate.opsForHash().put(key, field, value);
}
public Object hGet(String key, String field) {
return redisTemplate.opsForHash().get(key, field);
}
}
3.7 缓存预热(启动时加载)
package com.example.demo.config;
import com.example.demo.service.UserService;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
@Component
public class CacheWarmUp implements CommandLineRunner {
private final UserService userService;
public CacheWarmUp(UserService userService) {
this.userService = userService;
}
@Override
public void run(String... args) {
System.out.println("=== 缓存预热开始 ===");
userService.findAll(); // 预热用户列表缓存
System.out.println("=== 缓存预热完成 ===");
}
}
▶ 运行结果:
=== 缓存预热开始 ===
=== 缓存预热完成 ===
四、常见错误
4.1 缓存不生效
原因:
- 未在启动类添加
@EnableCaching - 调用方是类内部方法(自调用不走代理)
key表达式写错
解决:确保注解正确,避免自调用。
4.2 序列化异常
现象:从Redis取出的对象类型不对。
解决:配置正确的序列化器(如GenericJackson2JsonRedisSerializer),或在实体类中添加类型信息。
4.3 Redis连接失败
现象:Unable to connect to Redis
解决:检查Redis服务是否启动,spring.redis.host和port配置是否正确。
4.4 缓存与数据库不一致
原因:更新数据库后未及时更新/删除缓存。
解决:使用@CacheEvict在数据变更时清除缓存。
五、课后练习
- 为商品(Product)模块添加缓存,测试有缓存和无缓存的性能差异
- 实现一个缓存监控接口,返回Redis中缓存的key数量和内存占用
- 模拟缓存穿透:查询不存在的ID,观察数据库压力,然后添加空值缓存解决
- 使用
@CacheConfig统一配置缓存过期时间为1小时
六、本课小结
| 知识点 | 说明 |
|---|---|
| @Cacheable | 查询缓存,先查缓存再查库 |
| @CachePut | 更新缓存,每次执行方法后更新 |
| @CacheEvict | 清除缓存,数据变更时使用 |
| Redis | 内存数据库,高性能缓存方案 |
| 缓存穿透 | 查询不存在的数据,解决:缓存空值/布隆过滤器 |
| 缓存击穿 | 热key过期,解决:互斥锁/永不过期 |
| 缓存雪崩 | 大量key同时过期,解决:随机过期时间 |
本课程持续更新中,欢迎关注!