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)
✅ 处理缓存穿透/击穿/雪崩

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

相关推荐
alonewolf_999 小时前
Spring IOC容器扩展点全景:深入探索与实践演练
java·后端·spring
2501_9418024811 小时前
从缓存更新到数据一致性的互联网工程语法实践与多语言探索
java·后端·spring
Psycho_MrZhang12 小时前
页缓存技术(PageCache/sendfile/mmap)
缓存
蓝程序12 小时前
Spring AI学习 程序接入大模型
java·人工智能·spring
xiaolyuh12313 小时前
ThreadLocalMap 中弱引用被 GC 后的行为机制解析
java·jvm·redis
步步为营DotNet13 小时前
深度解析.NET中MemoryCache:高效缓存策略与性能优化的关键
缓存·性能优化·.net
会飞的胖达喵13 小时前
Redis 协议详解与 Telnet 直接连redis
数据库·redis·redis协议
wangbing112513 小时前
redis的存储问题
数据库·redis·缓存
zs宝来了13 小时前
大厂面试实录:Spring Boot源码深度解析+Redis缓存架构+RAG智能检索,谢飞机的AI电商面试之旅
spring boot·redis·微服务·大厂面试·java面试·rag·spring ai
手握风云-13 小时前
JavaEE 进阶第八期:Spring MVC - Web开发的“交通枢纽”(二)
前端·spring·java-ee