Spring Cache + Redis 声明式缓存指南

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 依赖配置

Mavenpom.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>

Gradlebuild.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:仅淘汰设置了过期时间的 Key
  • allkeys-lru:淘汰所有 Key(推荐)
  • volatile-ttl:淘汰即将过期的 Key
  • noeviction:不淘汰,写入失败

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:缓存未生效

现象:每次调用都执行方法。

排查步骤

  1. 确认 @EnableCaching 是否添加
  2. 检查方法是否被 Spring 管理(不能是 private)
  3. 确认方法调用是否通过代理(同类调用会失效)

错误示例

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 最佳实践建议

✅ 推荐做法
  1. 合理设置 TTL:根据数据更新频率设置过期时间
  2. 使用 JSON 序列化:可读性好,便于调试
  3. 启用 sync = true:防止缓存击穿
  4. 缓存空值:防止缓存穿透
  5. 监控命中率:及时发现缓存效率问题
❌ 避免做法
  1. 缓存大对象:单个 Value 不超过 1MB
  2. 缓存频繁变化的数据:如实时库存
  3. 同类调用:会导致缓存失效
  4. 过度依赖缓存:数据库是最终数据源
性能优化建议
优化点 建议 收益
连接池 使用 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
开源项目

📌 总结

核心要点回顾

  1. Spring Cache 是抽象层,通过注解实现声明式缓存
  2. Redis 是分布式缓存的首选,适合微服务架构
  3. 核心注解@Cacheable(查询)、@CachePut(更新)、@CacheEvict(清除)
  4. 序列化推荐 Jackson,兼顾性能和可读性
  5. 防护三大问题 :穿透(缓存空值)、击穿(sync=true)、雪崩(随机TTL)

快速检查清单

复制代码
✅ 添加依赖(spring-boot-starter-data-redis + cache)
✅ 配置 Redis 连接(application.yml)
✅ 启用缓存注解(@EnableCaching)
✅ 配置序列化器(Jackson JSON)
✅ 设置 TTL 过期时间
✅ 添加监控指标(Actuator)
✅ 处理缓存穿透/击穿/雪崩

最后提醒:缓存不是银弹,合理使用才能发挥最大价值。始终以数据一致性为第一原则!

相关推荐
咖啡八杯1 天前
GoF设计模式——备忘录模式
java·后端·spring·设计模式
vivo互联网技术1 天前
从 10 分钟到 1 秒:ES 深度分页任意跳页的三轮优化实战
服务器·数据库·redis·elasticsearch·深度分页
Flittly2 天前
【AgentScope Java新手村系列】(16)从RAG到多路检索
java·spring boot·spring
咖啡八杯3 天前
GoF设计模式——中介者模式
java·后端·spring·设计模式
用户3074596982074 天前
Redis 延时队列详解
redis
烤代码的吐司君4 天前
Redis 数据结构 ZSet, BIT, HyperLogLog,Geo 空间数据
redis·后端
Flittly4 天前
【AgentScope Java新手村系列】(14)人机交互
java·spring boot·spring
leeyi6 天前
Checkpoint 机制:Agent 怎么在断电后接着跑
redis·aigc·agent
云技纵横7 天前
一个 @Async 让循环依赖暴雷:Spring 代理的暗坑
redis