给应用加速:Spring Boot集成缓存 (Caffeine & Redis) 实战

在之前的文章中,我们已经构建了一个功能相对完整的应用,它能处理Web请求、访问数据库、管理事务、发送异步消息。但随着用户量和数据量的增长,性能问题往往会逐渐显现。你是否发现:

  • 某些接口响应越来越慢,因为底层需要执行复杂的数据库查询?

  • 同一个用户的基本信息(很少变动)在短时间内被反复从数据库加载?

  • 调用外部依赖服务的API耗时较长,拖慢了整个请求的处理?

这些场景都指向了一个共同的性能瓶颈:频繁访问慢速资源或重复执行昂贵计算 。如果我们能将这些操作的结果缓存起来,下次需要时直接从缓存(通常是内存或像Redis这样的高速存储)读取,性能将得到显著提升。

想象一下,你经常需要查阅一本厚重的工具书(数据库/慢服务)里的某个定义(数据)。每次都去翻书很慢。如果你把最常用的几个定义抄在一张便签(缓存)上贴在桌子旁,下次需要时看一眼便签就行了,速度是不是快多了?

Spring Boot通过spring-boot-starter-cache提供了一套简洁、强大的缓存抽象,让我们能够通过简单的注解(如@Cacheable, @CachePut, @CacheEvict)就能为方法添加缓存逻辑,而无需关心底层的具体缓存实现(是内存缓存还是Redis)。

读完本文,你将学会:

  • 理解缓存的核心价值和常见策略(缓存命中、失效、穿透、雪崩)。

  • 掌握Spring Boot Cache抽象层的基本原理和优势。

  • 集成并使用基于内存的高性能缓存库Caffeine

  • 集成并使用流行的分布式缓存解决方案Redis

  • 熟练运用@Cacheable, @CachePut, @CacheEvict等核心注解管理缓存。

  • 了解如何配置缓存的过期时间、大小限制等策略。

准备好为你的应用插上缓存的翅膀,让它飞得更快了吗?

一、为什么需要缓存?核心价值与挑战

核心价值:

  1. 提升性能 (Performance Boost): 缓存通常存储在比原始数据源(如数据库、磁盘、远程服务)快得多的介质中(内存>Redis>SSD>HDD)。从缓存读取数据能极大缩短响应时间。

  2. 降低后端负载 (Reduced Backend Load): 通过服务缓存中的数据,减少了对数据库、外部API等后端资源的直接访问次数,降低了它们的压力,提高了系统的整体吞吐量。

  3. 提高可用性 (Increased Availability - 某种程度上): 即使后端数据源暂时不可用,如果缓存中有所需数据,应用仍然可以提供部分服务(取决于缓存策略和数据时效性要求)。

常见缓存策略与挑战:

  • 缓存命中率 (Hit Rate): 衡量缓存效果的关键指标。命中率越高,缓存带来的性能提升越明显。需要合理设计缓存Key和缓存策略来提高命中率。

  • 缓存失效策略 (Eviction Policy): 当缓存空间不足时,需要决定淘汰哪些缓存项。常见的策略有LRU(最近最少使用)、LFU(最不经常使用)、FIFO(先进先出)等。

  • 缓存更新策略: 当原始数据发生变化时,如何更新缓存?

    • 读时更新 (Cache-Aside Pattern): 应用程序先读缓存,缓存未命中则读数据库,然后将结果写入缓存。更新数据时,先更新数据库,然后删除 (invalidate) 缓存。下次读取时会重新加载。这是最常用的策略。

    • 写时更新 (Write-Through): 应用程序更新数据时,同时更新数据库和缓存。实现简单,但可能写入性能稍差。

    • 写后更新 (Write-Behind/Write-Back): 应用程序只更新缓存,由缓存系统异步地将更新批量写入数据库。写入性能最好,但可能丢失数据(如果缓存系统宕机)。

  • 缓存穿透 (Cache Penetration): 查询一个数据库中根本不存在 的数据。缓存中自然也没有,导致每次请求都直接打到数据库,缓存失去意义。解决方案: 缓存空结果(设置较短过期时间),布隆过滤器。

  • 缓存击穿 (Cache Breakdown): 一个热点Key 在缓存过期失效的瞬间,大量并发请求同时涌入去查询数据库,导致数据库压力骤增。解决方案: 分布式锁(只允许一个请求去加载数据并写缓存),热点数据永不过期(后台异步刷新)。

  • 缓存雪崩 (Cache Avalanche): 大量缓存Key在同一时间集体失效 (例如,设置了相同的固定过期时间),导致所有请求瞬间全部打到数据库,造成数据库崩溃。解决方案: 设置随机的过期时间,多级缓存,限流降级。

  • 数据一致性: 缓存数据与数据库数据之间可能存在短暂的不一致。需要根据业务容忍度选择合适的更新策略。

二、Spring Cache 抽象:屏蔽底层差异

Spring Cache抽象的核心思想是面向注解编程。开发者只需要在需要缓存的方法上添加几个简单的注解,Spring就会在运行时通过AOP代理自动处理缓存的读写逻辑。

优势:

  • 代码侵入性低: 业务代码无需关心具体的缓存API,保持简洁。

  • 易于切换缓存实现: 只需修改配置和依赖,即可从内存缓存切换到Redis缓存,无需修改业务代码。

  • 声明式缓存: 缓存逻辑通过注解声明,清晰易懂。

核心注解:

  • @EnableCaching: 在配置类上启用Spring Cache功能。

  • @Cacheable(cacheNames="...", key="..."): 主要用于查询操作。方法执行前,会根据cacheNames和key检查缓存。

    • 如果缓存命中,直接返回缓存中的值,方法体不会执行

    • 如果缓存未命中,执行方法体,并将方法的返回值放入缓存,然后返回结果。

  • @CachePut(cacheNames="...", key="..."): 主要用于更新 操作。无论缓存是否存在,方法体都会执行 ,并将方法的返回值更新到缓存中。适用于希望保持缓存与最新数据同步的场景。

  • @CacheEvict(cacheNames="...", key="...", allEntries=false, beforeInvocation=false): 主要用于删除/失效操作。根据cacheNames和key从缓存中移除数据。

    • allEntries = true: 清空指定cacheNames下的所有缓存项。

    • beforeInvocation = true: 在方法执行前清除缓存(默认为false,即方法成功执行后清除)。

  • @Caching: 用于组合多个缓存注解在同一个方法上。

  • @CacheConfig: 类级别的注解,可以统一定义该类下所有缓存操作的cacheNames等公共属性。

缓存Key的生成:

Spring Cache默认会根据方法参数生成Key。对于无参方法,使用SimpleKey.EMPTY。对于有参方法,使用参数的hashCode()和equals()组合生成。通常需要自定义Key以获得更好的控制和可读性。可以使用SpEL (Spring Expression Language) 来自定义Key。

常用SpEL表达式:

  • #root.methodName: 当前方法名。

  • #root.args: 方法参数数组。

  • #argName 或 #pX: 按名称或索引访问方法参数(需要编译时保留参数名或使用索引)。

  • #result: (仅用于@CachePut, @CacheEvict的condition/unless)方法的返回值。

三、实战1:集成高性能内存缓存 Caffeine

Caffeine是一个基于Java 8开发的高性能、近乎最优的本地(内存)缓存库。非常适合单体应用或对缓存一致性要求不高的场景。

1. 添加依赖:

复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
</dependency>

2. 启用缓存 (@EnableCaching):

在你的主启动类或任何一个配置类上添加@EnableCaching。

复制代码
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching; // 导入

@SpringBootApplication
@EnableCaching // 启用缓存功能
public class YourApplication {
    public static void main(String[] args) {
        SpringApplication.run(YourApplication.class, args);
    }
}

3. 配置Caffeine (application.yml - 可选但推荐):

可以配置缓存的规格,如初始容量、最大容量、过期策略等。

复制代码
spring:
  cache:
    type: caffeine # 明确指定使用caffeine (如果类路径有多个缓存实现)
    cache-names: # (可选) 预定义缓存名称, 否则会自动创建
      - users
      - products
    caffeine:
      spec: > # 使用Caffeine的规格字符串进行配置
        initialCapacity=100, # 初始容量
        maximumSize=500, # 最大缓存条目数
        expireAfterWrite=10m, # 写入后10分钟过期
        # expireAfterAccess=5m, # 最后访问后5分钟过期
        recordStats # (可选) 开启缓存统计信息 (可通过 /actuator/caches 查看)
# 可以为不同的cache-name定义不同的spec, 但配置稍复杂, 通常通过代码配置CacheManager Bean实现

如果省略spring.cache.caffeine.spec,Caffeine会使用默认配置。更复杂的、针对不同cacheName的个性化配置通常通过自定义CacheManager Bean来实现。

4. 在Service方法上使用缓存注解:

复制代码
package com.example.service;

import com.example.model.User;
import com.example.repository.UserRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Optional;

@Service
public class UserService {

    private static final Logger log = LoggerFactory.getLogger(UserService.class);
    private final UserRepository userRepository;
    public static final String CACHE_NAME_USERS = "users"; // 定义缓存名称常量

    @Autowired
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    // --- @Cacheable ---
    // cacheNames/value: 指定缓存的名称 (可以有多个)
    // key: 自定义缓存Key, 使用SpEL。'user:'是前缀, #id是方法参数id的值
    // unless: 条件表达式 (SpEL), 如果为true, 则不缓存结果 (例如, 结果为空时不缓存)
    @Cacheable(cacheNames = CACHE_NAME_USERS, key = "'user:' + #id", unless = "#result == null")
    public User getUserById(Long id) {
        log.info("Fetching user from DB for id: {}", id); // 缓存未命中时会打印
        Optional<User> userOptional = userRepository.findById(id);
        return userOptional.orElse(null);
    }

    // --- @CachePut ---
    // 每次都会执行方法体, 并将返回值更新到缓存
    // 通常用于更新操作后刷新缓存
    @Transactional
    @CachePut(cacheNames = CACHE_NAME_USERS, key = "'user:' + #result.id") // 使用返回值的id作为key
    public User updateUser(User user) {
        log.info("Updating user in DB: {}", user);
        // 假设userRepository.save执行更新逻辑并返回更新后的User对象
        User updatedUser = userRepository.save(user);
        return updatedUser;
    }

    // --- @CacheEvict ---
    // 用于删除缓存项
    @Transactional
    @CacheEvict(cacheNames = CACHE_NAME_USERS, key = "'user:' + #id") // 删除指定id的缓存
    public void deleteUser(Long id) {
        log.info("Deleting user from DB for id: {}", id);
        userRepository.deleteById(id);
        // 方法执行成功后, 对应的缓存项会被移除
    }

    // --- @CacheEvict (清空缓存) ---
    @CacheEvict(cacheNames = CACHE_NAME_USERS, allEntries = true)
    public void clearUserCache() {
        log.info("Clearing all entries from users cache.");
        // 通常用于批量操作或需要强制刷新所有缓存的场景
    }

    // @Cacheable 的另一个例子: 使用用户名作为Key
    @Cacheable(cacheNames = CACHE_NAME_USERS, key = "'user:name:' + #name", unless = "#result == null")
    public User findUserByName(String name) {
        log.info("Fetching user from DB by name: {}", name);
        // 假设有 findByName 方法
        return userRepository.findByName(name);
    }
}

现在,当你多次调用getUserById(1L)时,只有第一次会打印"Fetching user from DB...",后续调用会直接从Caffeine内存缓存返回结果,速度极快。

四、实战2:集成分布式缓存 Redis

当应用需要部署多个实例(集群)时,内存缓存(如Caffeine)无法在实例间共享,每个实例都有自己的缓存副本,可能导致数据不一致。这时就需要分布式缓存 ,如Redis。Redis是一个高性能的内存键值数据库,常被用作分布式缓存、消息队列、分布式锁等。

1. 添加依赖:

复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <!-- 排除 Lettuce (默认连接池), 如果想用 Jedis -->
    <!-- <exclusions>
        <exclusion>
            <groupId>io.lettuce</groupId>
            <artifactId>lettuce-core</artifactId>
        </exclusion>
    </exclusions> -->
</dependency>
<!-- 如果排除了 Lettuce, 则需要引入 Jedis -->
<!-- <dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
</dependency> -->

spring-boot-starter-data-redis 包含了与Redis交互的核心库 (默认使用Lettuce连接池) 以及对Spring Cache的支持。

2. 配置Redis连接信息 (application.yml):

复制代码
spring:
  cache:
    type: redis # 明确指定使用redis
  redis:
    host: localhost # Redis服务器地址
    port: 6379      # Redis端口
    # password: your_redis_password # 如果有密码
    # database: 0   # 使用的Redis数据库索引 (默认0)
    # lettuce: # (可选) 配置Lettuce连接池
    #   pool:
    #     max-active: 8
    #     max-idle: 8
    #     min-idle: 0
    #     max-wait: -1ms # 负数表示无限等待

3. 启用缓存 (@EnableCaching): (如果之前没加过)

确保你的应用已经启用了缓存。

4. (重要)配置Redis序列化方式:

Spring Boot默认使用JDK序列化来存储缓存对象到Redis,这有几个缺点:可读性差、占用空间大、存在安全风险、跨语言不兼容。强烈推荐 配置为JSON序列化

在配置类中自定义RedisCacheManager Bean:

复制代码
package com.example.config;

import org.springframework.cache.CacheManager;
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
public class RedisCacheConfig {

    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        // 1. 配置序列化器
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                // 配置缓存键的序列化器 (通常使用String)
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                // 配置缓存值的序列化器 (使用JSON)
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
                // (可选) 配置默认的缓存过期时间
                .entryTtl(Duration.ofMinutes(30)); // 默认30分钟过期
                // (可选) 禁止缓存 null 值 (防止缓存穿透)
                // .disableCachingNullValues();

        // 2. 构建 RedisCacheManager
        RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
                .cacheDefaults(config) // 应用默认配置
                // (可选) 为特定的缓存名称配置不同的过期时间等
                // .withCacheConfiguration("users", config.entryTtl(Duration.ofHours(1)))
                // .withCacheConfiguration("products", config.entryTtl(Duration.ofDays(1)))
                .build();

        return cacheManager;
    }
}

这个配置将确保你的缓存Key是可读的字符串,缓存Value是以JSON格式存储在Redis中。

5. 在Service方法上使用缓存注解:
好消息是,你之前为Caffeine编写的带有@Cacheable, @CachePut, @CacheEvict注解的UserService代码,无需任何修改,就可以直接在Redis缓存下工作! 这就是Spring Cache抽象的魅力所在。

当你运行应用并调用相关方法时,Spring Cache会自动将数据缓存到配置好的Redis服务器中。你可以使用Redis客户端(如redis-cli)查看缓存内容(由于配置了JSON序列化,值是可读的JSON字符串)。

五、选择Caffeine还是Redis?

  • Caffeine (内存缓存):

    • 优点: 速度极快(内存读写),无网络开销,集成简单。

    • 缺点: 缓存数据只在当前应用实例内存中,无法跨实例共享;应用重启缓存丢失;缓存容量受限于应用内存。

    • 适用场景: 单体应用,对一致性要求不高但追求极致性能的场景,或者作为多级缓存的第一级(L1缓存)。

  • Redis (分布式缓存):

    • 优点: 缓存数据独立存储,可被多个应用实例共享;数据可持久化(配置得当);支持更丰富的数据结构;容量可扩展。

    • 缺点: 相比内存缓存有网络开销,速度稍慢(但仍然非常快);需要额外部署和维护Redis服务。

    • 适用场景: 分布式/集群应用,需要跨实例共享缓存,对数据一致性有一定要求,需要持久化或更大缓存容量的场景。

实践中,也常将两者结合使用(多级缓存): 先查Caffeine (L1),未命中再查Redis (L2),都未命中再查数据库。Spring Cache本身不直接支持多级缓存,但可以通过自定义CacheManager或使用第三方库实现。

六、总结:为性能插上翅膀

缓存是提升应用性能、降低后端负载的必备利器。Spring Boot Cache提供了一套优雅的抽象,通过简单的注解即可为方法添加缓存逻辑,同时屏蔽了底层缓存实现的差异。无论是高性能的内存缓存Caffeine,还是强大的分布式缓存Redis,都可以通过简单的配置和依赖集成到Spring Boot应用中。

理解缓存的核心概念、挑战和Spring Cache注解的使用,并根据应用场景(单体/分布式,一致性要求,性能目标)选择合适的缓存技术(或组合),将使你能够有效地利用缓存为应用加速。

相关推荐
xbhog9 分钟前
Java大厂面试突击:从Spring Boot自动配置到Kafka分区策略实战解析
spring boot·kafka·mybatis·java面试·分布式架构
lovebugs15 分钟前
Redis的高性能奥秘:深入解析IO多路复用与单线程事件驱动模型
redis·后端·面试
bug菌19 分钟前
面十年开发候选人被反问:当类被标注为@Service后,会有什么好处?我...🫨
spring boot·后端·spring
爱的叹息20 分钟前
MyBatis缓存配置的完整示例,包含一级缓存、二级缓存、自定义缓存策略等核心场景,并附详细注释和总结表格
缓存·mybatis
mask哥23 分钟前
详解最新链路追踪skywalking框架介绍、架构、环境本地部署&配置、整合微服务springcloudalibaba 、日志收集、自定义链路追踪、告警等
java·spring cloud·架构·gateway·springboot·skywalking·链路追踪
XU磊26026 分钟前
javaWeb开发---前后端开发全景图解(基础梳理 + 技术体系)
java·idea
学也不会29 分钟前
雪花算法
java·数据库·oracle
晓华-warm37 分钟前
国产免费工作流引擎star 5.9k,Warm-Flow版本升级1.7.0(新增大量好用功能)
java·中间件·流程图·开源软件·flowable·工作流·activities
凭君语未可40 分钟前
介绍 IntelliJ IDEA 快捷键操作
java·ide·intellij-idea
码上飞扬1 小时前
Java大师成长计划之第5天:Java中的集合框架
java·开发语言