Spring Cache + Redis 声明式缓存指南
📖 目录
- [1. 背景与动机](#1. 背景与动机)
- [2. 核心概念与架构](#2. 核心概念与架构)
- [3. 快速开始](#3. 快速开始)
- [4. 核心注解详解](#4. 核心注解详解)
- [5. 配置与定制化](#5. 配置与定制化)
- [6. 完整代码示例](#6. 完整代码示例)
- [7. 高级特性](#7. 高级特性)
- [8. 生产实践与优化](#8. 生产实践与优化)
- [9. 对比与扩展](#9. 对比与扩展)
1. 背景与动机
1.1 为什么需要缓存
在高并发 Web 应用中,数据库往往成为性能瓶颈:
| 问题 | 影响 | 解决方案 |
|---|---|---|
| 频繁查询 | 数据库连接耗尽 | 缓存热点数据 |
| 慢查询 | 接口响应时间长 | 缓存计算结果 |
| 高并发读 | 数据库 CPU/IO 打满 | 读写分离 + 缓存 |
典型收益:
- 响应时间:从 100ms 降至 5ms(20 倍提升)
- 数据库负载:减少 60%-90%
- 并发能力:从 1000 QPS 提升至 10000+ QPS
1.2 Spring Cache 的设计理念
Spring Cache 是一个 抽象层,核心思想是:
应用代码 → Spring Cache 抽象层 → 具体缓存实现(Redis/EhCache/Caffeine)
优势:
- ✅ 声明式编程:通过注解而非侵入式代码
- ✅ 实现无关:可无缝切换缓存提供者
- ✅ AOP 实现:自动拦截方法调用
- ✅ 统一 API:学习成本低
1.3 为什么选择 Redis
| 特性 | Redis | EhCache | Caffeine |
|---|---|---|---|
| 分布式支持 | ✅ 原生支持 | ❌ 仅单机 | ❌ 仅单机 |
| 持久化 | ✅ RDB/AOF | ⚠️ 有限 | ❌ 纯内存 |
| 数据结构 | ✅ 丰富(String/Hash/List等) | ⚠️ 简单 | ⚠️ 简单 |
| 高可用 | ✅ 哨兵/集群 | ❌ | ❌ |
| 适用场景 | 分布式系统 | 单体应用 | 单体应用 |
Redis 适用于:微服务架构、多实例部署、需要数据共享的场景。
2. 核心概念与架构
2.1 Spring Cache 抽象层
┌─────────────────────────────────────────┐
│ 应用代码(@Cacheable) │
├─────────────────────────────────────────┤
│ Spring Cache 抽象层(AOP) │
│ ┌─────────────┐ ┌──────────────┐ │
│ │CacheManager │ ───> │ Cache │ │
│ └─────────────┘ └──────────────┘ │
├─────────────────────────────────────────┤
│ 缓存提供者实现(SPI) │
│ ┌──────────┐ ┌──────────┐ ┌───────┐│
│ │ Redis │ │ EhCache │ │Caffeine││
│ └──────────┘ └──────────┘ └───────┘│
└─────────────────────────────────────────┘
2.2 核心接口
CacheManager(缓存管理器)
java
public interface CacheManager {
Cache getCache(String name); // 根据名称获取缓存
Collection<String> getCacheNames();
}
Cache(缓存操作接口)
java
public interface Cache {
ValueWrapper get(Object key); // 获取缓存
void put(Object key, Object value); // 写入缓存
void evict(Object key); // 清除缓存
}
2.3 Redis 作为缓存提供者
RedisCacheManager 是 Spring Data Redis 提供的实现:
java
RedisCacheManager
├─ RedisCacheConfiguration (配置)
│ ├─ TTL (过期时间)
│ ├─ KeySerializer (Key 序列化)
│ └─ ValueSerializer (Value 序列化)
└─ RedisConnectionFactory (连接工厂)
3. 快速开始
3.1 依赖配置
Maven (pom.xml):
xml
<dependencies>
<!-- Spring Boot Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 缓存抽象层 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- 连接池(推荐 Lettuce) -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- JSON 序列化(推荐) -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
Gradle (build.gradle):
gradle
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.boot:spring-boot-starter-cache'
implementation 'org.apache.commons:commons-pool2'
implementation 'com.fasterxml.jackson.core:jackson-databind'
}
3.2 Redis 连接配置
application.yml:
yaml
spring:
# Redis 连接配置
data:
redis:
host: localhost
port: 6379
password: your_password # 如无密码可省略
database: 0
timeout: 5000ms
# Lettuce 连接池配置(推荐)
lettuce:
pool:
max-active: 20 # 最大连接数
max-idle: 10 # 最大空闲连接
min-idle: 5 # 最小空闲连接
max-wait: 2000ms # 连接超时时间
# 缓存配置
cache:
type: redis # 指定缓存类型
redis:
time-to-live: 600000 # 默认过期时间 10 分钟(毫秒)
cache-null-values: true # 是否缓存空值(防止缓存穿透)
key-prefix: "app:" # Key 前缀
use-key-prefix: true # 是否使用前缀
3.3 启用缓存注解
在启动类或配置类添加 @EnableCaching:
java
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
@SpringBootApplication
@EnableCaching // ⭐ 启用缓存功能
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
4. 核心注解详解
4.1 @EnableCaching
作用:激活 Spring Cache 功能,触发 AOP 代理。
原理:
java
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(CachingConfigurationSelector.class)
public @interface EnableCaching {
boolean proxyTargetClass() default false; // 是否强制使用 CGLIB 代理
AdviceMode mode() default AdviceMode.PROXY; // 代理模式
}
4.2 @Cacheable(查询缓存)
作用:先查缓存,未命中则执行方法并缓存结果。
常用属性:
| 属性 | 说明 | 示例 |
|---|---|---|
cacheNames/value |
缓存名称 | @Cacheable("users") |
key |
缓存 Key(SpEL) | key = "#id" |
condition |
缓存条件 | condition = "#id > 0" |
unless |
排除条件 | unless = "#result == null" |
sync |
同步加载(防击穿) | sync = true |
示例:
java
@Service
public class UserService {
/**
* 根据 ID 查询用户(带缓存)
* Key: users::1
* TTL: 使用全局配置(10 分钟)
*/
@Cacheable(cacheNames = "users", key = "#id")
public User getUserById(Long id) {
System.out.println("从数据库查询用户: " + id);
return userRepository.findById(id).orElse(null);
}
/**
* 条件缓存:仅缓存 VIP 用户
*/
@Cacheable(cacheNames = "vipUsers",
key = "#id",
condition = "#user.isVip()")
public User getVipUser(Long id) {
return userRepository.findById(id).orElse(null);
}
/**
* 排除空值:不缓存查询结果为 null 的情况
*/
@Cacheable(cacheNames = "users",
key = "#username",
unless = "#result == null")
public User getUserByUsername(String username) {
return userRepository.findByUsername(username);
}
}
执行流程:
1. 拦截方法调用
2. 根据 Key 查询缓存
3. 命中 → 直接返回
4. 未命中 → 执行方法 → 缓存结果 → 返回
4.3 @CachePut(更新缓存)
作用:无论缓存是否存在,都执行方法并更新缓存。
典型场景:更新操作后同步缓存。
java
@Service
public class UserService {
/**
* 更新用户信息(同步更新缓存)
*/
@CachePut(cacheNames = "users", key = "#user.id")
public User updateUser(User user) {
System.out.println("更新数据库: " + user.getId());
return userRepository.save(user);
}
}
与 @Cacheable 的区别:
| 注解 | 执行方法 | 缓存操作 |
|---|---|---|
@Cacheable |
仅在缓存未命中时执行 | 读取 + 写入 |
@CachePut |
始终执行 | 仅写入 |
4.4 @CacheEvict(清除缓存)
作用:删除指定缓存数据。
常用属性:
| 属性 | 说明 | 示例 |
|---|---|---|
allEntries |
清空整个缓存 | allEntries = true |
beforeInvocation |
方法执行前清除 | beforeInvocation = true |
java
@Service
public class UserService {
/**
* 删除用户(清除缓存)
*/
@CacheEvict(cacheNames = "users", key = "#id")
public void deleteUser(Long id) {
System.out.println("删除用户: " + id);
userRepository.deleteById(id);
}
/**
* 批量导入用户(清空整个缓存)
*/
@CacheEvict(cacheNames = "users", allEntries = true)
public void importUsers(List<User> users) {
userRepository.saveAll(users);
}
/**
* 方法执行前清除缓存(防止异常导致缓存不一致)
*/
@CacheEvict(cacheNames = "users",
key = "#id",
beforeInvocation = true)
public void dangerousOperation(Long id) {
// 可能抛出异常的操作
}
}
4.5 @Caching(组合操作)
作用:组合多个缓存注解。
java
@Service
public class UserService {
/**
* 复杂场景:同时更新多个缓存
*/
@Caching(
put = {
@CachePut(cacheNames = "users", key = "#user.id"),
@CachePut(cacheNames = "usersByEmail", key = "#user.email")
},
evict = {
@CacheEvict(cacheNames = "userList", allEntries = true)
}
)
public User updateUserComplex(User user) {
return userRepository.save(user);
}
}
5. 配置与定制化
5.1 RedisCacheConfiguration 核心配置
完整配置类:
java
package com.example.demo.config;
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;
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
// 默认配置
RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig()
// Key 序列化(String)
.serializeKeysWith(
RedisSerializationContext.SerializationPair.fromSerializer(
new StringRedisSerializer()
)
)
// Value 序列化(JSON)
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(
new GenericJackson2JsonRedisSerializer()
)
)
// 默认过期时间 10 分钟
.entryTtl(Duration.ofMinutes(10))
// 禁用缓存空值
.disableCachingNullValues()
// Key 前缀
.prefixCacheNameWith("app:");
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(defaultConfig)
.build();
}
}
5.2 序列化方案选择
方案对比
| 序列化器 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| JDK | 原生支持 | 可读性差、体积大 | 简单场景 |
| Jackson | 可读性好、跨语言 | 性能中等 | ⭐ 推荐 |
| Protobuf | 高性能、体积小 | 需要定义 proto | 高性能要求 |
| Kryo | 高性能 | 需要注册类 | Java 专用 |
Jackson 序列化配置(推荐)
java
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
@Bean
public GenericJackson2JsonRedisSerializer jackson2JsonRedisSerializer() {
ObjectMapper objectMapper = new ObjectMapper();
// 启用类型信息(防止反序列化时类型丢失)
objectMapper.activateDefaultTyping(
LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL,
JsonTypeInfo.As.PROPERTY
);
return new GenericJackson2JsonRedisSerializer(objectMapper);
}
Redis 中的 JSON 数据示例:
json
{
"@class": "com.example.demo.entity.User",
"id": 1,
"username": "alice",
"email": "alice@example.com",
"createTime": ["java.time.LocalDateTime", "2024-01-01T10:00:00"]
}
5.3 TTL 过期时间配置
全局 TTL
java
RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10)); // 10 分钟
不同缓存空间使用不同 TTL
java
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
// 默认配置(10 分钟)
RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10));
// 用户缓存(1 小时)
RedisCacheConfiguration userConfig = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(1));
// 热点数据缓存(5 分钟)
RedisCacheConfiguration hotConfig = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(5));
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(defaultConfig)
.withCacheConfiguration("users", userConfig) // users 缓存使用 1 小时
.withCacheConfiguration("products", hotConfig) // products 缓存使用 5 分钟
.build();
}
5.4 Key 生成策略
默认 Key 生成规则
cacheNames::key
例如:users::1
自定义 Key 生成器
java
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.Arrays;
@Component("customKeyGenerator")
public class CustomKeyGenerator implements KeyGenerator {
@Override
public Object generate(Object target, Method method, Object... params) {
// 格式:类名:方法名:参数1_参数2
return target.getClass().getSimpleName() + ":" +
method.getName() + ":" +
Arrays.toString(params).replaceAll("[\\[\\]\\s]", "");
}
}
使用自定义 Key 生成器:
java
@Cacheable(cacheNames = "users", keyGenerator = "customKeyGenerator")
public User getUserById(Long id) {
return userRepository.findById(id).orElse(null);
}
5.5 多 CacheManager 配置
场景:同时使用 Redis(分布式) + Caffeine(本地)。
java
@Configuration
public class MultiCacheConfig {
// Redis CacheManager
@Bean
@Primary
public CacheManager redisCacheManager(RedisConnectionFactory factory) {
return RedisCacheManager.builder(factory)
.cacheDefaults(RedisCacheConfiguration.defaultCacheConfig())
.build();
}
// Caffeine CacheManager
@Bean
public CacheManager caffeineCacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(Caffeine.newBuilder()
.expireAfterWrite(5, TimeUnit.MINUTES)
.maximumSize(1000));
return cacheManager;
}
}
指定使用哪个 CacheManager:
java
@Cacheable(cacheNames = "users", cacheManager = "redisCacheManager")
public User getUserById(Long id) {
return userRepository.findById(id).orElse(null);
}
6. 完整代码示例
6.1 项目结构
src/main/java/com/example/demo
├── DemoApplication.java # 启动类
├── config
│ └── CacheConfig.java # 缓存配置
├── entity
│ └── User.java # 实体类
├── repository
│ └── UserRepository.java # 数据访问层
├── service
│ └── UserService.java # 业务层(带缓存)
└── controller
└── UserController.java # 控制器
6.2 配置类
java
package com.example.demo.config;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
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.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
// 配置 Jackson 序列化器
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.activateDefaultTyping(
LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL,
JsonTypeInfo.As.PROPERTY
);
GenericJackson2JsonRedisSerializer jsonSerializer =
new GenericJackson2JsonRedisSerializer(objectMapper);
// 默认缓存配置
RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(
RedisSerializationContext.SerializationPair.fromSerializer(
new StringRedisSerializer()
)
)
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(jsonSerializer)
)
.entryTtl(Duration.ofMinutes(10))
.disableCachingNullValues()
.prefixCacheNameWith("app:");
// 用户缓存配置(1 小时)
RedisCacheConfiguration userConfig = defaultConfig
.entryTtl(Duration.ofHours(1));
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(defaultConfig)
.withCacheConfiguration("users", userConfig)
.transactionAware() // 支持事务
.build();
}
}
6.3 实体类
java
package com.example.demo.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import jakarta.persistence.*;
import java.io.Serializable;
import java.time.LocalDateTime;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "t_user")
public class User implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true, length = 50)
private String username;
@Column(nullable = false, length = 100)
private String email;
@Column(name = "create_time")
private LocalDateTime createTime;
@Column(name = "update_time")
private LocalDateTime updateTime;
}
6.4 Repository 层
java
package com.example.demo.repository;
import com.example.demo.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
}
6.5 Service 层(核心)
java
package com.example.demo.service;
import com.example.demo.entity.User;
import com.example.demo.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.*;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
@Slf4j
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
/**
* 查询用户(带缓存)
* Key: users::1
*/
@Cacheable(cacheNames = "users", key = "#id", unless = "#result == null")
public User getUserById(Long id) {
log.info("从数据库查询用户: {}", id);
return userRepository.findById(id).orElse(null);
}
/**
* 根据用户名查询(带缓存)
*/
@Cacheable(cacheNames = "usersByName", key = "#username")
public User getUserByUsername(String username) {
log.info("从数据库查询用户: {}", username);
return userRepository.findByUsername(username).orElse(null);
}
/**
* 查询所有用户(带缓存)
*/
@Cacheable(cacheNames = "userList")
public List<User> getAllUsers() {
log.info("从数据库查询所有用户");
return userRepository.findAll();
}
/**
* 创建用户(清除列表缓存)
*/
@Transactional
@CacheEvict(cacheNames = "userList", allEntries = true)
public User createUser(User user) {
user.setCreateTime(LocalDateTime.now());
user.setUpdateTime(LocalDateTime.now());
log.info("创建用户: {}", user.getUsername());
return userRepository.save(user);
}
/**
* 更新用户(更新缓存 + 清除列表缓存)
*/
@Transactional
@Caching(
put = {
@CachePut(cacheNames = "users", key = "#user.id"),
@CachePut(cacheNames = "usersByName", key = "#user.username")
},
evict = {
@CacheEvict(cacheNames = "userList", allEntries = true)
}
)
public User updateUser(User user) {
user.setUpdateTime(LocalDateTime.now());
log.info("更新用户: {}", user.getId());
return userRepository.save(user);
}
/**
* 删除用户(清除所有相关缓存)
*/
@Transactional
@Caching(evict = {
@CacheEvict(cacheNames = "users", key = "#id"),
@CacheEvict(cacheNames = "userList", allEntries = true)
})
public void deleteUser(Long id) {
log.info("删除用户: {}", id);
userRepository.deleteById(id);
}
}
6.6 Controller 层
java
package com.example.demo.controller;
import com.example.demo.entity.User;
import com.example.demo.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
return userService.getUserById(id);
}
@GetMapping("/username/{username}")
public User getUserByUsername(@PathVariable String username) {
return userService.getUserByUsername(username);
}
@GetMapping
public List<User> getAllUsers() {
return userService.getAllUsers();
}
@PostMapping
public User createUser(@RequestBody User user) {
return userService.createUser(user);
}
@PutMapping
public User updateUser(@RequestBody User user) {
return userService.updateUser(user);
}
@DeleteMapping("/{id}")
public void deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
}
}
6.7 测试验证
测试类:
java
package com.example.demo.service;
import com.example.demo.entity.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
class UserServiceTest {
@Autowired
private UserService userService;
@Test
void testCache() {
// 第一次查询(从数据库)
User user1 = userService.getUserById(1L);
assertNotNull(user1);
// 第二次查询(从缓存,不会打印日志)
User user2 = userService.getUserById(1L);
assertEquals(user1.getId(), user2.getId());
}
}
查看 Redis 缓存:
bash
# 连接 Redis
redis-cli
# 查看所有 Key
127.0.0.1:6379> KEYS app:*
1) "app:users::1"
2) "app:usersByName::alice"
3) "app:userList"
# 查看缓存内容
127.0.0.1:6379> GET "app:users::1"
"{\"@class\":\"com.example.demo.entity.User\",\"id\":1,\"username\":\"alice\",\"email\":\"alice@example.com\",\"createTime\":[\"java.time.LocalDateTime\",\"2024-01-01T10:00:00\"],\"updateTime\":[\"java.time.LocalDateTime\",\"2024-01-01T10:00:00\"]}"
# 查看 TTL(剩余过期时间)
127.0.0.1:6379> TTL "app:users::1"
(integer) 3599 # 还剩 3599 秒(约 1 小时)
7. 高级特性
7.1 缓存失效策略
主动失效策略
| 策略 | 实现方式 | 适用场景 |
|---|---|---|
| TTL 过期 | entryTtl(Duration) |
时效性数据 |
| LRU 淘汰 | Redis maxmemory-policy |
内存受限 |
| 手动清除 | @CacheEvict |
数据更新后 |
Redis 淘汰策略配置
redis.conf:
conf
maxmemory 2gb
maxmemory-policy allkeys-lru # LRU 算法淘汰任意 Key
常用策略:
volatile-lru:仅淘汰设置了过期时间的 Keyallkeys-lru:淘汰所有 Key(推荐)volatile-ttl:淘汰即将过期的 Keynoeviction:不淘汰,写入失败
7.2 SpEL 表达式应用
常用 SpEL 表达式:
| 表达式 | 说明 | 示例 |
|---|---|---|
#参数名 |
方法参数 | key = "#id" |
#p0, #p1 |
参数索引 | key = "#p0" |
#result |
方法返回值 | unless = "#result == null" |
#root.methodName |
方法名 | key = "#root.methodName + #id" |
#root.targetClass |
目标类 | - |
复杂示例:
java
@Cacheable(
cacheNames = "users",
key = "#root.targetClass.simpleName + ':' + #root.methodName + ':' + #id",
condition = "#id > 0 && #includeDeleted == false",
unless = "#result == null || #result.id == null"
)
public User getUserById(Long id, boolean includeDeleted) {
return userRepository.findById(id).orElse(null);
}
7.3 条件缓存(condition/unless)
condition(缓存前置条件)
java
// 仅缓存 ID > 0 的用户
@Cacheable(cacheNames = "users", key = "#id", condition = "#id > 0")
public User getUserById(Long id) {
return userRepository.findById(id).orElse(null);
}
unless(缓存排除条件)
java
// 不缓存 null 结果和管理员用户
@Cacheable(
cacheNames = "users",
key = "#id",
unless = "#result == null || #result.role == 'ADMIN'"
)
public User getUserById(Long id) {
return userRepository.findById(id).orElse(null);
}
两者区别:
| 属性 | 执行时机 | 适用场景 |
|---|---|---|
condition |
方法执行前 | 根据参数决定是否缓存 |
unless |
方法执行后 | 根据返回值决定是否缓存 |
7.4 自定义 CacheResolver
场景:动态选择缓存名称。
java
import org.springframework.cache.interceptor.CacheResolver;
import org.springframework.cache.interceptor.CacheOperationInvocationContext;
import org.springframework.stereotype.Component;
@Component("dynamicCacheResolver")
public class DynamicCacheResolver implements CacheResolver {
@Autowired
private CacheManager cacheManager;
@Override
public Collection<? extends Cache> resolveCaches(
CacheOperationInvocationContext<?> context) {
// 根据参数动态选择缓存名称
Object[] args = context.getArgs();
String cacheName = (boolean) args[1] ? "vipUsers" : "normalUsers";
return Collections.singletonList(cacheManager.getCache(cacheName));
}
}
使用自定义 Resolver:
java
@Cacheable(cacheResolver = "dynamicCacheResolver", key = "#id")
public User getUserById(Long id, boolean isVip) {
return userRepository.findById(id).orElse(null);
}
8. 生产实践与优化
8.1 缓存穿透/击穿/雪崩的防护
1️⃣ 缓存穿透(查询不存在的数据)
问题:恶意请求不存在的 Key,每次都打到数据库。
解决方案:
java
// 方案 1:缓存空值
@Cacheable(cacheNames = "users", key = "#id")
public User getUserById(Long id) {
User user = userRepository.findById(id).orElse(null);
return user != null ? user : new User(); // 返回空对象而非 null
}
// 方案 2:布隆过滤器(需要单独实现)
@Autowired
private BloomFilter<Long> userIdBloomFilter;
public User getUserById(Long id) {
if (!userIdBloomFilter.mightContain(id)) {
return null; // 直接返回,不查数据库
}
return userRepository.findById(id).orElse(null);
}
配置缓存空值:
yaml
spring:
cache:
redis:
cache-null-values: true # 允许缓存 null
2️⃣ 缓存击穿(热点 Key 过期)
问题:热点数据过期瞬间,大量请求同时打到数据库。
解决方案 :使用 sync = true 启用同步锁。
java
@Cacheable(
cacheNames = "hotUsers",
key = "#id",
sync = true // ⭐ 只有一个线程查询数据库
)
public User getHotUser(Long id) {
log.info("查询热点用户: {}", id);
return userRepository.findById(id).orElse(null);
}
原理:
请求1 → 未命中 → 获取锁 → 查询DB → 写缓存 → 释放锁
请求2 → 未命中 → 等待锁 ────────────────────> 读缓存
请求3 → 未命中 → 等待锁 ────────────────────> 读缓存
3️⃣ 缓存雪崩(大量 Key 同时过期)
问题:大量缓存同时失效,数据库瞬间压力剧增。
解决方案:设置随机过期时间。
java
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10 + new Random().nextInt(5))); // 10-15 分钟随机
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
}
对比表:
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 缓存穿透 | 查询不存在的数据 | 缓存空值 / 布隆过滤器 |
| 缓存击穿 | 热点 Key 过期 | sync = true |
| 缓存雪崩 | 大量 Key 同时过期 | 随机 TTL |
8.2 性能监控与指标
启用 Redis 监控
application.yml:
yaml
management:
endpoints:
web:
exposure:
include: health,info,metrics,caches
metrics:
enable:
cache: true
查看缓存指标
访问端点:
bash
# 查看缓存统计
curl http://localhost:8080/actuator/caches
# 查看缓存命中率
curl http://localhost:8080/actuator/metrics/cache.gets?tag=result:hit
curl http://localhost:8080/actuator/metrics/cache.gets?tag=result:miss
自定义监控
java
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.stereotype.Component;
@Component
public class CacheMetrics {
@Autowired
private MeterRegistry meterRegistry;
public void recordCacheHit(String cacheName) {
meterRegistry.counter("cache.hit", "cache", cacheName).increment();
}
public void recordCacheMiss(String cacheName) {
meterRegistry.counter("cache.miss", "cache", cacheName).increment();
}
}
8.3 常见问题排查
问题 1:缓存未生效
现象:每次调用都执行方法。
排查步骤:
- 确认
@EnableCaching是否添加 - 检查方法是否被 Spring 管理(不能是 private)
- 确认方法调用是否通过代理(同类调用会失效)
错误示例:
java
@Service
public class UserService {
// ❌ 同类调用,缓存失效
public void businessMethod() {
getUserById(1L); // 直接调用,不走代理
}
@Cacheable(cacheNames = "users", key = "#id")
public User getUserById(Long id) {
return userRepository.findById(id).orElse(null);
}
}
正确做法:
java
@Service
public class UserService {
@Autowired
private UserService self; // 注入自己
public void businessMethod() {
self.getUserById(1L); // 通过代理调用
}
}
问题 2:序列化异常
现象:
SerializationException: Could not read JSON: Unrecognized field...
解决方案:
java
// 实体类添加无参构造器
@NoArgsConstructor
@AllArgsConstructor
public class User implements Serializable {
// ...
}
问题 3:缓存不过期
现象:数据一直不更新。
排查:
bash
# 查看 Key 的 TTL
redis-cli TTL "app:users::1"
(integer) -1 # -1 表示永不过期
解决:检查配置是否正确设置 TTL。
8.4 最佳实践建议
✅ 推荐做法
- 合理设置 TTL:根据数据更新频率设置过期时间
- 使用 JSON 序列化:可读性好,便于调试
- 启用
sync = true:防止缓存击穿 - 缓存空值:防止缓存穿透
- 监控命中率:及时发现缓存效率问题
❌ 避免做法
- 缓存大对象:单个 Value 不超过 1MB
- 缓存频繁变化的数据:如实时库存
- 同类调用:会导致缓存失效
- 过度依赖缓存:数据库是最终数据源
性能优化建议
| 优化点 | 建议 | 收益 |
|---|---|---|
| 连接池 | 使用 Lettuce + 连接池 | 减少连接开销 |
| 序列化 | 使用 JSON 代替 JDK | 减少 50% 空间 |
| 批量操作 | 使用 Pipeline | 提升 10 倍吞吐 |
| 本地缓存 | Caffeine + Redis 二级缓存 | 减少网络开销 |
9. 对比与扩展
9.1 与 EhCache/Caffeine 对比
| 特性 | Redis | EhCache | Caffeine |
|---|---|---|---|
| 部署模式 | 独立服务 | 嵌入式 | 嵌入式 |
| 数据共享 | ✅ 多实例共享 | ❌ 单实例 | ❌ 单实例 |
| 持久化 | ✅ 支持 | ⚠️ 有限 | ❌ |
| 性能 | 网络延迟(1-5ms) | 极快(纳秒级) | 极快(纳秒级) |
| 内存管理 | 独立进程 | 占用 JVM 堆 | 占用 JVM 堆 |
| 适用场景 | 分布式系统 | 单体应用 | 单体应用 |
选择建议:
- 分布式系统 → Redis
- 单体应用 + 小数据量 → Caffeine
- 需要持久化 + 单机 → EhCache
9.2 多级缓存架构
架构设计:
请求 → Caffeine (本地缓存) → Redis (分布式缓存) → 数据库
↑ 命中返回 ↑ 命中返回 ↑ 未命中查询
实现示例:
java
@Configuration
public class MultiLevelCacheConfig {
@Bean
@Primary
public CacheManager cacheManager(
CacheManager redisCacheManager,
CacheManager caffeineCacheManager) {
return new CompositeCacheManager(
caffeineCacheManager, // 优先查询本地缓存
redisCacheManager // 本地未命中再查 Redis
);
}
}
9.3 进阶阅读资源
官方文档
推荐书籍
- 《Redis 设计与实现》- 黄健宏
- 《Spring Boot 实战》- Craig Walls
开源项目
📌 总结
核心要点回顾
- Spring Cache 是抽象层,通过注解实现声明式缓存
- Redis 是分布式缓存的首选,适合微服务架构
- 核心注解 :
@Cacheable(查询)、@CachePut(更新)、@CacheEvict(清除) - 序列化推荐 Jackson,兼顾性能和可读性
- 防护三大问题 :穿透(缓存空值)、击穿(
sync=true)、雪崩(随机TTL)
快速检查清单
✅ 添加依赖(spring-boot-starter-data-redis + cache)
✅ 配置 Redis 连接(application.yml)
✅ 启用缓存注解(@EnableCaching)
✅ 配置序列化器(Jackson JSON)
✅ 设置 TTL 过期时间
✅ 添加监控指标(Actuator)
✅ 处理缓存穿透/击穿/雪崩
最后提醒:缓存不是银弹,合理使用才能发挥最大价值。始终以数据一致性为第一原则!