一、缓存概述
1. 什么是缓存?
缓存:将数据存储在内存中,以减少数据库查询次数,提高系统性能。
2. 为什么需要缓存?
| 场景 | 问题 | 缓存解决方案 |
|---|---|---|
| 频繁查询相同数据 | 每次都查询数据库,响应慢 | 缓存查询结果 |
| 数据不常变化 | 数据库压力大 | 缓存减少数据库压力 |
| 高并发访问 | 数据库成为瓶颈 | 缓存分担负载 |
| 热点数据 | 大量请求访问同一数据 | 缓存提供快速响应 |
3. 缓存类型对比
| 缓存类型 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 本地缓存(Caffeine) | 速度快、无需网络 | 容量有限、无法共享 | 单机应用 |
| 分布式缓存(Redis) | 容量大、可共享 | 需要网络、延迟略高 | 分布式应用 |
二、Spring Cache 简介
1. 核心注解
| 注解 | 作用 | 示例 |
|---|---|---|
@Cacheable | 缓存方法返回值 | @Cacheable("users") |
@CachePut | 更新缓存 | @CachePut(value = "users", key = "#id") |
@CacheEvict | 清除缓存 | @CacheEvict(value = "users", key = "#id") |
@Caching | 组合多个缓存操作 | @Caching(cacheable = {...}, evict = {...}) |
@CacheConfig | 类级别缓存配置 | @CacheConfig(cacheNames = "users") |
2. 添加依赖
<!-- 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>
<!-- Caffeine(本地缓存) -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
3. 启用缓存
启动类添加注解:
@SpringBootApplication
@EnableCaching // 启用缓存
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
三、Redis 缓存配置
1. Redis 配置
application.properties:
# Redis 配置
spring.data.redis.host=localhost
spring.data.redis.port=6379
spring.data.redis.password=
spring.data.redis.database=0
# Redis 连接池配置
spring.data.redis.lettuce.pool.max-active=8
spring.data.redis.lettuce.pool.max-idle=8
spring.data.redis.lettuce.pool.min-idle=0
spring.data.redis.lettuce.pool.max-wait=-1ms
# 缓存配置
spring.cache.type=redis
spring.cache.redis.time-to-live=600000 # 10 分钟
spring.cache.redis.cache-null-values=false # 不缓存空值
spring.cache.redis.key-prefix="myapp:" # Key 前缀
spring.cache.redis.use-key-prefix=true
2. Redis 配置类
RedisConfig.java:
package com.example.myapp.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.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.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
@Configuration
@EnableCaching
public class RedisConfig {
// RedisTemplate 配置
@Bean
public RedisTemplate<String, Object> redisTemplate(
RedisConnectionFactory connectionFactory
) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
// JSON 序列化配置
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer =
new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.activateDefaultTyping(
LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL
);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
// Key 使用 String 序列化
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// Key 采用 String 序列化
template.setKeySerializer(stringRedisSerializer);
// Hash Key 采用 String 序列化
template.setHashKeySerializer(stringRedisSerializer);
// Value 采用 Jackson 序列化
template.setValueSerializer(jackson2JsonRedisSerializer);
// Hash Value 采用 Jackson 序列化
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
// RedisCacheManager 配置
@Bean
public RedisCacheManager cacheManager(
RedisConnectionFactory connectionFactory
) {
// JSON 序列化配置
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer =
new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.activateDefaultTyping(
LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL
);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
// 配置缓存
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10)) // 默认过期时间 10 分钟
.serializeKeysWith(RedisSerializationContext.SerializationPair
.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(jackson2JsonRedisSerializer))
.disableCachingNullValues(); // 不缓存空值
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(config)
.build();
}
}
四、缓存注解使用
1. @Cacheable(缓存读取)
场景:查询用户时缓存结果
@Service
public class UserService {
// 缓存查询结果
@Cacheable(
value = "users", // 缓存名称
key = "#id", // 缓存 Key(使用方法参数 id)
unless = "#result == null" // 结果为 null 时不缓存
)
public UserDTO findById(Long id) {
// 第一次执行:查询数据库
User user = userRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("User", id));
return UserDTO.fromEntity(user);
// 结果被缓存到 Redis
}
}
工作原理:
1. 调用 findById(1)
↓
2. 检查缓存中是否存在 key="users::1"
↓
3. 存在:直接返回缓存数据(不执行方法)
不存在:执行方法,查询数据库
↓
4. 将结果写入缓存
↓
5. 第二次调用 findById(1):直接从缓存返回
2. @CachePut(更新缓存)
场景:更新用户时更新缓存
@Service
public class UserService {
// 更新缓存
@CachePut(
value = "users",
key = "#id"
)
public UserDTO update(Long id, UpdateUserDTO userDTO) {
User user = userRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("User", id));
// 更新用户
if (userDTO.getName() != null) {
user.setName(userDTO.getName());
}
User updatedUser = userRepository.save(user);
// 结果更新到缓存
return UserDTO.fromEntity(updatedUser);
}
}
工作原理:
1. 调用 update(1, userDTO)
↓
2. 执行方法,更新数据库
↓
3. 将新结果更新到缓存 key="users::1"
↓
4. 下次查询:获取最新的缓存数据
3. @CacheEvict(清除缓存)
场景:删除用户时清除缓存
@Service
public class UserService {
// 清除缓存
@CacheEvict(
value = "users",
key = "#id"
)
public void delete(Long id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("User", id));
userRepository.delete(user);
// 缓存 key="users::id" 被清除
}
// 清除所有缓存
@CacheEvict(
value = "users",
allEntries = true
)
public void clearAllCache() {
// 清除 users 缓存下的所有数据
}
}
4. @Caching(组合操作)
场景:同时更新多个缓存
@Service
public class UserService {
@Caching(
cacheable = {
@Cacheable(value = "users", key = "#id")
},
evict = {
@CacheEvict(value = "userList", allEntries = true)
}
)
public UserDTO findById(Long id) {
// 缓存到 users,同时清除 userList 缓存
}
}
5. @CacheConfig(类级别配置)
场景:统一配置缓存的 cacheNames
@Service
@CacheConfig(cacheNames = "users")
public class UserService {
// 不需要指定 value
@Cacheable(key = "#id")
public UserDTO findById(Long id) {
// ...
}
@CachePut(key = "#id")
public UserDTO update(Long id, UpdateUserDTO userDTO) {
// ...
}
@CacheEvict(key = "#id")
public void delete(Long id) {
// ...
}
}
五、自定义缓存 Key
1. Key 表达式
| 表达式 | 说明 | 示例 |
|---|---|---|
#id | 使用方法参数 id 的值 | @Cacheable(key = "#id") |
#user.id | 使用 user 对象的 id 属性 | @Cacheable(key = "#user.id") |
#root.methodName | 使用方法名 | @Cacheable(key = "#root.methodName") |
#root.args[0] | 使用第一个参数 | @Cacheable(key = "#root.args[0]") |
#p0 | 使用第一个参数(简写) | @Cacheable(key = "#p0") |
T(Math).random() | 调用静态方法 | @Cacheable(key = "user_" + #id + "_" + T(Math).random()) |
2. 自定义 Key 生成器
CustomKeyGenerator.java:
package com.example.myapp.config;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.StringJoiner;
@Component
public class CustomKeyGenerator implements KeyGenerator {
@Override
public Object generate(Object target, Method method, Object... params) {
StringBuilder builder = new StringBuilder();
builder.append(target.getClass().getSimpleName());
builder.append(".");
builder.append(method.getName());
builder.append("(");
StringJoiner joiner = new StringJoiner(",");
for (Object param : params) {
joiner.add(param != null ? param.toString() : "null");
}
builder.append(joiner);
builder.append(")");
return builder.toString();
}
}
使用自定义 Key 生成器:
@Cacheable(
value = "users",
keyGenerator = "customKeyGenerator"
)
public UserDTO findById(Long id) {
// Key: UserService.findById(1)
}
六、Caffeine 本地缓存
1. Caffeine 配置
application.properties:
# Caffeine 配置
spring.cache.type=caffeine
spring.cache.cache-names=users,products
spring.cache.caffeine.spec=maximumSize=1000,expireAfterWrite=10m
2. Caffeine 配置类
CaffeineConfig.java:
package com.example.myapp.config;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.TimeUnit;
@Configuration
@EnableCaching
public class CaffeineConfig {
@Bean
public Caffeine<Object, Object> caffeineConfig() {
return Caffeine.newBuilder()
.initialCapacity(100) // 初始容量
.maximumSize(1000) // 最大容量
.expireAfterWrite(10, TimeUnit.MINUTES) // 写入后 10 分钟过期
.recordStats(); // 记录缓存统计信息
}
@Bean
public CacheManager cacheManager(Caffeine<Object, Object> caffeine) {
CaffeineCacheManager caffeineCacheManager = new CaffeineCacheManager();
caffeineCacheManager.setCaffeine(caffeine);
return caffeineCacheManager;
}
}
3. Caffeine 使用示例
@Service
public class UserService {
@Cacheable(value = "users", key = "#id")
public UserDTO findById(Long id) {
// 使用 Caffeine 本地缓存
}
}
七、Redis 缓存 vs Caffeine 缓存
| 维度 | Redis 缓存 | Caffeine 缓存 |
|---|---|---|
| 类型 | 分布式缓存 | 本地缓存 |
| 速度 | 略慢(网络延迟) | 极快(内存) |
| 容量 | 大(取决于服务器) | 小(取决于 JVM 堆内存) |
| 共享 | 多实例共享 | 单机独享 |
| 持久化 | 支持久化 | 不支持 |
| 适用场景 | 分布式系统 | 单机应用、热点数据 |
八、缓存穿透、雪崩、击穿
1. 缓存穿透
定义:查询不存在的数据,每次都绕过缓存直接查询数据库。
解决方案:
- 缓存空值(
unless = "#result == null") - 布隆过滤器
@Cacheable(
value = "users",
key = "#id",
unless = "#result == null" // 结果为 null 时不缓存
)
public UserDTO findById(Long id) {
// ...
}
2. 缓存雪崩
定义:大量缓存同时失效,所有请求都打到数据库。
解决方案:
- 设置不同的过期时间
- 使用互斥锁(Redis SETNX)
@Cacheable(
value = "users",
key = "#id",
unless = "#result == null"
)
public UserDTO findById(Long id) {
// 随机过期时间
long randomExpire = 10 + (long) (Math.random() * 5);
// ...
}
3. 缓存击穿
定义:热点数据过期,大量请求同时查询该数据。
解决方案:
- 互斥锁(Redis SETNX)
- 永不过期(后台更新)
@Cacheable(
value = "users",
key = "#id",
sync = true // 同步缓存更新
)
public UserDTO findById(Long id) {
// ...
}
九、缓存实战示例
1. 完整的 UserService
package com.example.myapp.service;
import com.example.myapp.dto.CreateUserDTO;
import com.example.myapp.dto.UpdateUserDTO;
import com.example.myapp.dto.UserDTO;
import com.example.myapp.exception.ResourceNotFoundException;
import com.example.myapp.model.User;
import com.example.myapp.repository.UserRepository;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.stream.Collectors;
@Service
@Transactional
@CacheConfig(cacheNames = "users")
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
// 缓存查询
@Cacheable(key = "#id", unless = "#result == null")
public UserDTO findById(Long id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("User", id));
return UserDTO.fromEntity(user);
}
// 创建用户(清除缓存)
@CacheEvict(allEntries = true)
public UserDTO create(CreateUserDTO userDTO) {
if (userRepository.existsByEmail(userDTO.getEmail())) {
throw new IllegalArgumentException("邮箱已被使用: " + userDTO.getEmail());
}
User user = new User();
user.setName(userDTO.getName());
user.setEmail(userDTO.getEmail());
user.setPassword(userDTO.getPassword());
user.setPhone(userDTO.getPhone());
User savedUser = userRepository.save(user);
return UserDTO.fromEntity(savedUser);
}
// 更新用户(更新缓存)
@CachePut(key = "#id")
public UserDTO update(Long id, UpdateUserDTO userDTO) {
User user = userRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("User", id));
if (userDTO.getEmail() != null &&
!userDTO.getEmail().equals(user.getEmail()) &&
userRepository.existsByEmail(userDTO.getEmail())) {
throw new IllegalArgumentException("邮箱已被使用: " + userDTO.getEmail());
}
if (userDTO.getName() != null) {
user.setName(userDTO.getName());
}
if (userDTO.getEmail() != null) {
user.setEmail(userDTO.getEmail());
}
if (userDTO.getPhone() != null) {
user.setPhone(userDTO.getPhone());
}
if (userDTO.getActive() != null) {
user.setActive(userDTO.getActive());
}
User updatedUser = userRepository.save(user);
return UserDTO.fromEntity(updatedUser);
}
// 删除用户(清除缓存)
@CacheEvict(key = "#id")
public void delete(Long id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("User", id));
userRepository.delete(user);
}
// 搜索用户(不缓存)
public List<UserDTO> searchByName(String name) {
return userRepository.findByNameContainingIgnoreCase(name).stream()
.map(UserDTO::fromEntity)
.collect(Collectors.toList());
}
// 清除所有缓存
@CacheEvict(allEntries = true)
public void clearAllCache() {
// 清除所有缓存
}
}
2. 测试缓存效果
测试代码:
@SpringBootTest
public class UserServiceTest {
@Autowired
private UserService userService;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Test
public void testCache() {
// 第一次查询:查询数据库
UserDTO user1 = userService.findById(1L);
// 检查 Redis 中是否存在缓存
String cacheKey = "users::1";
Object cachedUser = redisTemplate.opsForValue().get(cacheKey);
System.out.println("缓存中的数据: " + cachedUser);
// 第二次查询:从缓存读取
UserDTO user2 = userService.findById(1L);
// 清除缓存
userService.clearAllCache();
}
}
十、总结
| 概念 | 说明 |
|---|---|
| Spring Cache | Spring 缓存抽象层 |
| Redis 缓存 | 分布式缓存 |
| Caffeine 缓存 | 本地缓存 |
| @Cacheable | 缓存读取 |
| @CachePut | 更新缓存 |
| @CacheEvict | 清除缓存 |
| 缓存穿透 | 查询不存在的数据 |
| 缓存雪崩 | 大量缓存同时失效 |
| 缓存击穿 | 热点数据过期 |