当前位置: 首页 > article >正文

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

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

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

1. 引入依赖

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

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

2. 启用缓存

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

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

3. 常用注解

@Cacheable
  • 用于将方法的返回结果缓存起来。
  • 方法被调用时,Spring 会先检查缓存中是否有数据,如果有则直接返回缓存结果,否则执行方法并将结果放入缓存。
  • @Cacheable 本身并不直接提供过期时间的配置。缓存的过期时间取决于具体的缓存提供者。
@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
  • 用于强制更新缓存内容,方法每次都会执行,将返回值放入缓存。
  • 常用于方法更新数据后,同时更新缓存中的数据。
    
    @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
  • 用于清除缓存数据。
  • 通常用于删除或更新方法,用于从缓存中移除不再需要的数据。
    @Service
    public class UserService {
    
        @CacheEvict(value = "userCache", key = "#id")
        public void deleteUser(Long id) {
            // 删除数据库中的用户
            userRepository.deleteById(id);
        }
    }

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

@Caching
  • 用于组合多个缓存注解,适用于需要同时执行多个缓存操作的场景。

@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
@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.完整示例

@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 的依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2. 配置 Redis 连接

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

# 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,导致直接查询数据库缓存空结果、布隆过滤器、参数校验
缓存击穿热点数据过期,瞬时大量请求穿透缓存,打到数据库设置热点数据永不过期、加锁机制、双重检查
缓存雪崩缓存中大量数据同时失效,导致数据库请求激增设置过期时间加随机、缓存预热、分级降级策略、多层缓存

http://www.kler.cn/a/378884.html

相关文章:

  • VS code SSH设置多个远程连接免登录
  • 运维工具之docker入门
  • 【设计模式系列】组合模式(十二)
  • 语言模型的采样方法
  • el-table 滚动条重置 手动控制滚动条
  • Rust 力扣 - 643. 子数组最大平均数 I
  • 7-12 检查密码
  • LeetCode 203. 移除链表元素(java)
  • Android面试整理
  • 【热门主题】000027 React:前端框架的强大力量
  • [C++]:智能指针
  • 大数据之——Window电脑本地配置hadoop系统(100%包避坑!!方便日常测试,不用再去虚拟机那么麻烦)
  • Python画笔案例-095 绘制鼠标画笔
  • [java][基础]HTTPTomcatServlet
  • 高防服务器都有哪些类型?
  • Java 正则基础
  • 生成对抗网络(GAN)如何推动AIGC的发展
  • MacOS如何读取磁盘原始的扇区内容,恢复误删除的数据
  • 【IC每日一题--单bitCDC跨时钟和同步FIFO】
  • [ 应急响应靶场实战 ] VMware 搭建win server 2012应急响应靶机 攻击者获取服务器权限上传恶意病毒 防守方人员应急响应并溯源
  • ssm基于vue搭建的新闻网站+vue
  • Python+Selenium+Pytest+POM自动化测试框架封装
  • 机器学习的模型评估与选择
  • Msys mingw32编译报错 CMake Error: Could not create named generator MSYS Makefiles
  • DIP(Deep Image Prior,深度图像先验)和DMs(Diffusion Models,扩散模型)
  • CytoSPACE·单细胞与空间转录组的高精度对齐