Springboot 内置缓存与整合Redis作为缓存

Spring Boot 的缓存注解允许开发者在不修改业务逻辑的情况下,将方法的计算结果缓存起来,从而减少重复计算和数据库查询,提高系统性能。

1、Spring Boot Cache 的基本用法及常用注解

1. 引入依赖

首先,需要在项目中引入缓存相关依赖。

XML 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

2. 启用缓存

在 Spring Boot 主程序类上添加 @EnableCaching 注解,开启缓存支持:

java 复制代码
@SpringBootApplication
@EnableCaching
public class CacheApplication {
    public static void main(String[] args) {
        SpringApplication.run(CacheApplication.class, args);
    }
}

3. 常用注解

@Cacheable
  • 用于将方法的返回结果缓存起来。
  • 方法被调用时,Spring 会先检查缓存中是否有数据,如果有则直接返回缓存结果,否则执行方法并将结果放入缓存。
  • @Cacheable 本身并不直接提供过期时间的配置。缓存的过期时间取决于具体的缓存提供者。
java 复制代码
@Service
public class UserService {

    @Cacheable(value = "userCache", key = "#id")#根据id不同查找缓存,在同一个缓存空间(如 responseCache)内,可以存储多个结果,每个结果与唯一的缓存键对应(即不同的请求参数对应不同的缓存结果)。
    public User getUserById(Long id) {
        // 假设这是一个耗时的数据库查询
        return userRepository.findById(id).orElse(null);
    }
}

注:

  • @Cacheable:表示该方法的结果应该被缓存。Spring 在方法第一次被调用时会执行该方法并缓存其结果,后续对同一参数的调用将直接返回缓存中的结果。

  • value = "userCache" :指定缓存的名称,value 属性对应缓存的命名空间。这里的 userCache 是缓存的名称,用于存储此方法的返回结果。userCache 可以对应一个具体的缓存实现(如 Redis、EhCache 等)中一个独立的缓存区域。

  • key = "#id" :指定缓存的键。使用 Spring 表达式语言(SpEL)来定义缓存键的生成规则,#id 表示方法参数 id 的值作为缓存的键。

    • 在这里,#id 会直接使用方法参数 id 的值。例如,当 id=1 时,缓存键就是 1
    • 如果不指定 key 属性,Spring 会默认将所有方法参数的组合作为缓存键。

默认键生成机制

@Cacheable 注解没有指定 key 时,Spring Cache 会将所有方法参数的组合作为默认缓存键。具体来说,Spring Cache 使用 SimpleKeyGenerator 生成缓存键:

  1. 单一参数 :如果方法有一个参数,且未指定 key,Spring 会直接使用该参数作为缓存键。
  2. 多参数 :如果方法有多个参数,Spring 会将它们组合成一个 SimpleKey 对象作为缓存键。
  3. 无参数 :如果方法没有参数,Spring 会使用 SimpleKey.EMPTY 作为缓存键。

如果传入的是自定义对象参数,Spring 会调用该对象的 hashCodeequals 方法来识别不同参数值的唯一性,以确保生成的缓存键唯一。对于自定义对象,如果没有重写 equalshashCode 方法,则默认使用 Object 类的实现。ObjecthashCodeequals 方法基于对象的内存地址判断两个对象是否相等,这样不同实例即使内容相同,也会被认为是不同的对象。因此,不重写 equalshashCode 可能会导致缓存命中失败,从而产生重复的缓存条目。

  • 不指定 key 且传入自定义对象 :需要重写对象的 equalshashCode 方法,以确保相同内容的对象具有相同的缓存键。
  • 推荐方式 :如果传入对象属性确定,可以使用 SpEL 表达式明确指定缓存键(如 key = "#user.id"),这样更简洁并避免了对 equalshashCode 的依赖。
@CachePut
  • 用于强制更新缓存内容,方法每次都会执行,将返回值放入缓存。

  • 常用于方法更新数据后,同时更新缓存中的数据。

    java 复制代码
    @Service
    public class UserService {
        
        @CachePut(value = "userCache", key = "#user.id")
        public User updateUser(User user) {
            // 更新数据库中的用户信息
            return userRepository.save(user);
        }
    }

    @CachePut 注解表示方法执行完成后将结果更新到 userCache 中,key 为 user.id

@CacheEvict
  • 用于清除缓存数据。

  • 通常用于删除或更新方法,用于从缓存中移除不再需要的数据。

    java 复制代码
    @Service
    public class UserService {
    
        @CacheEvict(value = "userCache", key = "#id")
        public void deleteUser(Long id) {
            // 删除数据库中的用户
            userRepository.deleteById(id);
        }
    }

    @CacheEvict 会将缓存 userCache 中 key 为 id 的缓存项删除,确保缓存中的数据与数据库保持一致。

@Caching
  • 用于组合多个缓存注解,适用于需要同时执行多个缓存操作的场景。
java 复制代码
@Service
public class UserService {

    @Caching(
        put = { @CachePut(value = "userCache", key = "#user.id") },
        evict = { @CacheEvict(value = "userListCache", allEntries = true) }
    )
    public User saveUser(User user) {
        // 保存用户信息
        return userRepository.save(user);
    }
}

在此示例中,@Caching 组合了 @CachePut@CacheEvict,即在缓存 userCache 中更新用户数据,同时清除 userListCache 中的所有缓存项。

4. 自定义缓存键和条件

Spring Boot Cache 允许通过 keycondition 参数自定义缓存键和条件。

  • key:自定义缓存的 key,可以使用 SpEL 表达式。默认是方法参数的组合。
  • condition:满足指定条件时才缓存,使用 SpEL 表达式。
  • unless :满足条件时不缓存,优先级高于 condition
java 复制代码
@Cacheable(value = "userCache", key = "#user.id", condition = "#user.age > 18", unless = "#result == null")
public User getUser(User user) {
    return userRepository.findById(user.getId()).orElse(null);
}

在此例中,condition 表示只有用户年龄大于 18 时才缓存,unless 表示当方法返回值为 null 时不缓存。

5.完整示例

java 复制代码
@Service
public class UserService {

    @Cacheable(value = "userCache", key = "#id")
    public Optional<User> getUserById(Long id) {
        // 模拟数据库查询
        return userRepository.findById(id);
    }

    @CachePut(value = "userCache", key = "#user.id")
    public User updateUser(User user) {
        // 更新数据库中的用户信息
        return userRepository.save(user);
    }

    @CacheEvict(value = "userCache", key = "#id")
    public void deleteUser(Long id) {
        // 从数据库中删除用户信息
        userRepository.deleteById(id);
    }
}

缺点:本地缓存, 分布式场景容易产生数据不一致的情况。

2、Spring Boot 结合 Redis 实现缓存

为了解决缓存一致问题,可以引入分布式缓存Redis。

1. 引入依赖

项目中添加 Redis 的依赖:

XML 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2. 配置 Redis 连接

application.ymlapplication.properties 中配置 Redis 连接信息:

XML 复制代码
# Spring Cache 配置
spring.cache.type=redis   # 设置 Spring 缓存使用 Redis

# Redis 连接配置
spring.redis.host=localhost   # Redis 服务器地址
spring.redis.port=6379        # Redis 服务器端口
spring.redis.password=yourpassword   # Redis 连接密码,如果没有则省略
# Redis 缓存过期时间(可选)
spring.cache.redis.time-to-live=60000   # 设置缓存的默认过期时间(单位为毫秒),此处为 60 秒

spring.cache.redis.use-key-prefix=true    # 使用 key 前缀,果有多种业务数据存储在同一个 Redis 实例中,建议开启 key 前缀
spring.cache.redis.key-prefix=test_cache_  # 设置 Redis 缓存的 key 前缀

3. 启用缓存支持

同上

4. 使用缓存注解

同上

这样即可使用 Redis分布式缓存替换Spring Boot 自带缓存显著提升性能,解决分布式场景下数据不一致问题。

3、引入缓存带来的问题

缓存击穿

定义

缓存击穿 是指一个热点数据在缓存过期的瞬间,大量并发请求访问该数据,由于缓存刚好过期,导致请求同时涌入数据库查询。这个问题会导致数据库压力骤增,甚至崩溃。

原因
  • 热点数据在缓存失效的瞬间,大量请求同时访问该数据。
  • 高并发场景下,单一热点数据的缓存失效后数据库压力过大。
解决方案
  1. 设置热点数据永不过期(逻辑过期):对于少量非常热门的数据,可以设置缓存永不过期。然后通过后台异步线程来定时更新缓存,避免缓存失效导致的瞬间压力增大。
  2. 加锁机制:在缓存失效时,只有一个线程去数据库中查询数据并回填缓存,其他线程等待第一个线程完成后从缓存读取。可以使用分布式锁(如 Redis 分布式锁)来控制并发。
  3. 双重检查:在缓存失效时再进行二次检查。在缓存过期的情况下,多个线程查询缓存后都会去请求数据库,可以在第一次查询数据库后立即更新缓存,再次检查缓存以减少数据库的并发压力。
  4. **增加定时任务定期刷新缓存:**通过 Spring 的定时任务来实现定时刷新缓存,保证热点数据在缓存即将失效时被重新加载。

缓存穿透

定义

缓存穿透是指大量请求查询缓存中不存在的数据,导致请求直接穿透缓存到数据库,给数据库带来极大压力。例如,用户查询一个不存在的用户 ID,由于缓存中没有该数据且数据库也没有结果,每次查询都绕过缓存直接访问数据库。

原因
  • 查询的 key 不存在,缓存没有数据,直接查询数据库。
  • 恶意攻击或程序错误导致频繁查询不存在的 key。
解决方案
  1. 缓存空结果:对于数据库查询结果为空的数据,将空结果也缓存一段时间(例如 5 分钟)。这样,短时间内相同的请求不会频繁查询数据库。可以通过设置较短的过期时间来避免大量无效数据长期占用缓存。(spring.cache.redis.cache-null-values=true)
  2. 布隆过滤器:在缓存之前增加一个布隆过滤器,用于判断 key 是否可能存在。布隆过滤器可以有效过滤掉不存在的数据,避免直接查询数据库。
  3. 参数校验:对用户输入的 key 进行校验,防止恶意请求。比如查询数据库前对 key 做基础检查,确保格式和范围有效。

缓存雪崩

定义

缓存雪崩是指缓存中大量数据在同一时间失效,导致大量请求同时打到数据库,造成数据库压力激增。缓存雪崩可能会导致系统出现短暂或持续的不可用。

原因
  • 大量缓存设置了相同的过期时间,在某一时刻同时失效。
  • 系统重启或崩溃,导致所有缓存失效。
解决方案
  1. 缓存过期时间加随机:避免所有缓存设置相同的过期时间,可以在过期时间上增加一个随机值,使得缓存过期时间分散,避免集中失效。
  2. 缓存预热:在系统启动时,将常用的热点数据提前加载到缓存中,避免高峰时段突然访问数据库。
  3. 分级降级策略:在缓存雪崩发生时,可以使用限流策略和降级策略,限制数据库的访问频率。同时,也可以用备用缓存来缓解瞬时压力。
  4. 多层缓存架构:使用多级缓存(如本地缓存 + 分布式缓存)或多机房缓存,实现分布式缓存冗余,避免单点失效导致大量缓存失效。

总结:

问题 定义 解决方案
缓存穿透 请求大量不存在的 key,导致直接查询数据库 缓存空结果、布隆过滤器、参数校验
缓存击穿 热点数据过期,瞬时大量请求穿透缓存,打到数据库 设置热点数据永不过期、加锁机制、双重检查
缓存雪崩 缓存中大量数据同时失效,导致数据库请求激增 设置过期时间加随机、缓存预热、分级降级策略、多层缓存
相关推荐
Bug退退退12324 分钟前
RabbitMQ 高级特性之重试机制
java·分布式·spring·rabbitmq
全栈凯哥36 分钟前
02.SpringBoot常用Utils工具类详解
java·spring boot·后端
hello早上好2 小时前
CGLIB代理核心原理
java·spring
在肯德基吃麻辣烫2 小时前
《Redis》缓存与分布式锁
redis·分布式·缓存
RainbowSea3 小时前
跨域问题(Allow CORS)解决(3 种方法)
java·spring boot·后端
RainbowSea3 小时前
问题 1:MyBatis-plus-3.5.9 的分页功能修复
java·spring boot·mybatis
sniper_fandc5 小时前
SpringBoot系列—入门
java·spring boot·后端
先睡8 小时前
Redis的缓存击穿和缓存雪崩
redis·spring·缓存
Albert Edison10 小时前
【最新版】IntelliJ IDEA 2025 创建 SpringBoot 项目
java·spring boot·intellij-idea
Bug退退退12312 小时前
RabbitMQ 高级特性之死信队列
java·分布式·spring·rabbitmq