0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

SpringBoot项目中使用缓存的正确方法

jf_ro2CN3Fa 来源:芋道源码 2023-06-13 10:59 次阅读

前言

缓存可以通过将经常访问的数据存储在内存中,减少底层数据源如数据库的压力,从而有效提高系统的性能和稳定性。我想大家的项目中或多或少都有使用过,我们项目也不例外,但是最近在review公司的代码的时候写的很蠢且low, 大致写法如下:

publicUsergetById(Stringid){
Useruser=cache.getUser();
if(user!=null){
returnuser;
}
//从数据库获取
user=loadFromDB(id);
cahce.put(id,user);
returnuser;
}

其实Spring Boot 提供了强大的缓存抽象,可以轻松地向您的应用程序添加缓存。本文就讲讲如何使用 Spring 提供的不同缓存注解实现缓存的最佳实践。

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

项目地址:https://github.com/YunaiV/ruoyi-vue-pro

视频教程:https://doc.iocoder.cn/video/

启用缓存@EnableCaching

现在大部分项目都是是SpringBoot项目,我们可以在启动类添加注解@EnableCaching来开启缓存功能。

@SpringBootApplication
@EnableCaching
publicclassSpringCacheApp{

publicstaticvoidmain(String[]args){
SpringApplication.run(Cache.class,args);
}
}

既然要能使用缓存,就需要有一个缓存管理器Bean,默认情况下,@EnableCaching 将注册一个ConcurrentMapCacheManager的Bean,不需要单独的 bean 声明。ConcurrentMapCacheManager将值存储在ConcurrentHashMap的实例中,这是缓存机制的最简单的线程安全实现。

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

项目地址:https://github.com/YunaiV/yudao-cloud

视频教程:https://doc.iocoder.cn/video/

自定义缓存管理器

默认的缓存管理器并不能满足需求,因为她是存储在jvm内存中的,那么如何存储到redis中呢?这时候需要添加自定义的缓存管理器。

添加依赖


org.springframework.boot
spring-boot-starter-data-redis

配置Redis缓存管理器

@Configuration
@EnableCaching
publicclassCacheConfig{

@Bean
publicRedisConnectionFactoryredisConnectionFactory(){
returnnewLettuceConnectionFactory();
}

@Bean
publicCacheManagercacheManager(){
RedisCacheConfigurationredisCacheConfiguration=RedisCacheConfiguration.defaultCacheConfig()
.disableCachingNullValues()
.serializeValuesWith(SerializationPair.fromSerializer(newGenericJackson2JsonRedisSerializer()));

RedisCacheManagerredisCacheManager=RedisCacheManager.builder(redisConnectionFactory())
.cacheDefaults(redisCacheConfiguration)
.build();

returnredisCacheManager;
}
}

现在有了缓存管理器以后,我们如何在业务层面操作缓存呢?

我们可以使用@Cacheable、@CachePut 或@CacheEvict 注解来操作缓存了。

@Cacheable

该注解可以将方法运行的结果进行缓存,在缓存时效内再次调用该方法时不会调用方法本身,而是直接从缓存获取结果并返回给调用方。

868c1144-098a-11ee-962d-dac502259ad0.png

例子1:缓存数据库查询的结果。

@Service
publicclassMyService{

@Autowired
privateMyRepositoryrepository;

@Cacheable(value="myCache",key="#id")
publicMyEntitygetEntityById(Longid){
returnrepository.findById(id).orElse(null);
}
}

在此示例中,@Cacheable 注解用于缓存 getEntityById()方法的结果,该方法根据其 ID 从数据库中检索 MyEntity 对象。

但是如果我们更新数据呢?旧数据仍然在缓存中?

@CachePut

然后@CachePut 出来了, 与 @Cacheable 注解不同的是使用 @CachePut 注解标注的方法,在执行前不会去检查缓存中是否存在之前执行过的结果,而是每次都会执行该方法,并将执行结果以键值对的形式写入指定的缓存中。@CachePut 注解一般用于更新缓存数据,相当于缓存使用的是写模式中的双写模式。

@Service
publicclassMyService{

@Autowired
privateMyRepositoryrepository;

@CachePut(value="myCache",key="#entity.id")
publicvoidsaveEntity(MyEntityentity){
repository.save(entity);
}
}

@CacheEvict

标注了 @CacheEvict 注解的方法在被调用时,会从缓存中移除已存储的数据。@CacheEvict 注解一般用于删除缓存数据,相当于缓存使用的是写模式中的失效模式。

869f9d36-098a-11ee-962d-dac502259ad0.png

@Service
publicclassMyService{

@Autowired
privateMyRepositoryrepository;

@CacheEvict(value="myCache",key="#id")
publicvoiddeleteEntityById(Longid){
repository.deleteById(id);
}
}

@Caching

@Caching 注解用于在一个方法或者类上,同时指定多个 Spring Cache 相关的注解。

86b9efb0-098a-11ee-962d-dac502259ad0.png

例子1:@Caching注解中的evict属性指定在调用方法 saveEntity 时失效两个缓存。

@Service
publicclassMyService{

@Autowired
privateMyRepositoryrepository;

@Cacheable(value="myCache",key="#id")
publicMyEntitygetEntityById(Longid){
returnrepository.findById(id).orElse(null);
}

@Caching(evict={
@CacheEvict(value="myCache",key="#entity.id"),
@CacheEvict(value="otherCache",key="#entity.id")
})
publicvoidsaveEntity(MyEntityentity){
repository.save(entity);
}

}

例子2:调用getEntityById方法时,Spring会先检查结果是否已经缓存在myCache缓存中。如果是,Spring 将返回缓存的结果而不是执行该方法。如果结果尚未缓存,Spring 将执行该方法并将结果缓存在 myCache 缓存中。方法执行后,Spring会根据@CacheEvict注解从otherCache缓存中移除缓存结果。

@Service
publicclassMyService{

@Caching(
cacheable={
@Cacheable(value="myCache",key="#id")
},
evict={
@CacheEvict(value="otherCache",key="#id")
}
)
publicMyEntitygetEntityById(Longid){
returnrepository.findById(id).orElse(null);
}

}

例子3:当调用saveData方法时,Spring会根据@CacheEvict注解先从otherCache缓存中移除数据。然后,Spring 将执行该方法并将结果保存到数据库或外部 API

方法执行后,Spring 会根据@CachePut注解将结果添加到 myCache、myOtherCache 和 myThirdCache 缓存中。Spring 还将根据@Cacheable注解检查结果是否已缓存在 myFourthCache 和 myFifthCache 缓存中。如果结果尚未缓存,Spring 会将结果缓存在适当的缓存中。如果结果已经被缓存,Spring 将返回缓存的结果,而不是再次执行该方法。

@Service
publicclassMyService{

@Caching(
put={
@CachePut(value="myCache",key="#result.id"),
@CachePut(value="myOtherCache",key="#result.id"),
@CachePut(value="myThirdCache",key="#result.name")
},
evict={
@CacheEvict(value="otherCache",key="#id")
},
cacheable={
@Cacheable(value="myFourthCache",key="#id"),
@Cacheable(value="myFifthCache",key="#result.id")
}
)
publicMyEntitysaveData(Longid,Stringname){
//CodetosavedatatoadatabaseorexternalAPI
MyEntityentity=newMyEntity(id,name);
returnentity;
}

}

@CacheConfig

通过@CacheConfig 注解,我们可以将一些缓存配置简化到类级别的一个地方,这样我们就不必多次声明相关值:

@CacheConfig(cacheNames={"myCache"})
@Service
publicclassMyService{

@Autowired
privateMyRepositoryrepository;

@Cacheable(key="#id")
publicMyEntitygetEntityById(Longid){
returnrepository.findById(id).orElse(null);
}

@CachePut(key="#entity.id")
publicvoidsaveEntity(MyEntityentity){
repository.save(entity);
}

@CacheEvict(key="#id")
publicvoiddeleteEntityById(Longid){
repository.deleteById(id);
}
}

Condition & Unless

condition作用:指定缓存的条件(满足什么条件才缓存),可用 SpEL 表达式(如 #id>0,表示当入参 id 大于 0 时才缓存)

unless作用 : 否定缓存,即满足 unless 指定的条件时,方法的结果不进行缓存,使用 unless 时可以在调用的方法获取到结果之后再进行判断(如 #result == null,表示如果结果为 null 时不缓存)

//whenid>10,the@CachePutworks.
@CachePut(key="#entity.id",condition="#entity.id>10")
publicvoidsaveEntity(MyEntityentity){
repository.save(entity);
}


//whenresult!=null,the@CachePutworks.
@CachePut(key="#id",condition="#result==null")
publicvoidsaveEntity1(MyEntityentity){
repository.save(entity);
}

清理全部缓存

通过allEntries、beforeInvocation属性可以来清除全部缓存数据,不过allEntries是方法调用后清理,beforeInvocation是方法调用前清理。

//方法调用完成之后,清理所有缓存
@CacheEvict(value="myCache",allEntries=true)
publicvoiddelectAll(){
repository.deleteAll();
}

//方法调用之前,清除所有缓存
@CacheEvict(value="myCache",beforeInvocation=true)
publicvoiddelectAll(){
repository.deleteAll();
}

SpEL表达式

Spring Cache注解中频繁用到SpEL表达式,那么具体如何使用呢?

SpEL 表达式的语法

86cdaee2-098a-11ee-962d-dac502259ad0.png

Spring Cache可用的变量

86e7950a-098a-11ee-962d-dac502259ad0.png

最佳实践

通过Spring缓存注解可以快速优雅地在我们项目中实现缓存的操作,但是在双写模式或者失效模式下,可能会出现缓存数据一致性问题(读取到脏数据),Spring Cache 暂时没办法解决。最后我们再总结下Spring Cache使用的一些最佳实践。

只缓存经常读取的数据:缓存可以显着提高性能,但只缓存经常访问的数据很重要。很少或从不访问的缓存数据会占用宝贵的内存资源,从而导致性能问题。

根据应用程序的特定需求选择合适的缓存提供程序和策略。SpringBoot 支持多种缓存提供程序,包括 Ehcache、Hazelcast 和 Redis。

使用缓存时请注意潜在的线程安全问题。对缓存的并发访问可能会导致数据不一致或不正确,因此选择线程安全的缓存提供程序并在必要时使用适当的同步机制非常重要。

避免过度缓存。缓存对于提高性能很有用,但过多的缓存实际上会消耗宝贵的内存资源,从而损害性能。在缓存频繁使用的数据和允许垃圾收集不常用的数据之间取得平衡很重要。

使用适当的缓存逐出策略。使用缓存时,重要的是定义适当的缓存逐出策略以确保在必要时从缓存中删除旧的或陈旧的数据。

使用适当的缓存键设计。缓存键对于每个数据项都应该是唯一的,并且应该考虑可能影响缓存数据的任何相关参数,例如用户 ID、时间或位置。

常规数据(读多写少、即时性与一致性要求不高的数据)完全可以使用 Spring Cache,至于写模式下缓存数据一致性问题的解决,只要缓存数据有设置过期时间就足够了。

特殊数据(读多写多、即时性与一致性要求非常高的数据),不能使用 Spring Cache,建议考虑特殊的设计(例如使用 Cancal 中间件等)。
责任编辑:彭菁

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 缓存
    +关注

    关注

    1

    文章

    220

    浏览量

    26444
  • 代码
    +关注

    关注

    30

    文章

    4556

    浏览量

    66814
  • 数据源
    +关注

    关注

    1

    文章

    59

    浏览量

    9590
  • SpringBoot
    +关注

    关注

    0

    文章

    172

    浏览量

    106

原文标题:SpringBoot项目中使用缓存的正确姿势,太优雅了!

文章出处:【微信号:芋道源码,微信公众号:芋道源码】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    如何在XPS项目中使用SmartXplorer

    您好Xilinx社区,我对使用SmartXplorer方法试图改善设计时间感兴趣。我似乎能够在命令行工具上找到大量信息,并且我了解如何从命令行运行程序等。但是,我似乎无法找到的是如何在XPS项目中使
    发表于 10-17 14:14

    labview项目中的类怎么才能够迁移到另一个项目使用

    项目中的类怎么才能够起一道另一个项目中使用,是只能重新在另一个项目中创建么,搞了一天没有好的方法操作
    发表于 06-03 10:14

    如何在我的项目中使用停止模式?

    你好,我想在我的项目中使用停止模式。有什么例子吗?我想让我的外围模块在初始化时停止模式。如果用户将唤醒按钮,模块醒来并开始广告。模块进入停止模式,再然后preconfiguredtimeout已过期。
    发表于 09-25 14:58

    项目中使用SYSBIOS有好处吗?

    我找了下ControlSuite发现没有相应的例程,尤其是驱动部分,另外在项目中使用SYSBIOS有好处吗?
    发表于 06-01 06:49

    SpringBoot应用启动运行run方法

    什么时候创建嵌入式的Servlet容器工厂?什么时候获取嵌入式的Servlet容器并启动Tomcat;获取嵌入式的Servlet容器工厂:1)、SpringBoot应用启动运行run方法2
    发表于 12-20 06:16

    分享一下在项目中使用串口接收数据及处理的方法

    在定时器中断里需要做哪些事呢?怎样在项目中使用串口接收数据及处理呢?
    发表于 02-24 06:17

    在ESP-IDF项目中使用BSON有什么想法吗?

    组件。我需要按块从相机发送照片,所以我需要将每个块都以二进制格式放置。在 ESP-IDF 项目中使用 BSON 有什么想法吗?
    发表于 03-01 06:30

    如何在ESP-IDF项目中使用BSON ?

    组件。我需要按块从相机发送照片,所以我需要将每个块都以二进制格式放置。在 ESP-IDF 项目中使用 BSON 有什么想法吗?
    发表于 04-14 06:59

    使用Method Swizzling遇到的问题和项目中使用的Swizzling方案

    导语:Method Swizzling是Objective-C中运行时中讨论较多的内容,本文主要介绍使用Method Swizzling遇到的问题和项目中使用的Swizzling方案
    发表于 09-22 19:35 0次下载
    使用Method Swizzling遇到的问题和<b class='flag-5'>项目中使</b>用的Swizzling方案

    如何在SpringBoot项目中实现动态定时任务

    之前写过文章记录怎么在SpringBoot项目中简单使用定时任务,不过由于要借助cron表达式且都提前定义好放在配置文件里,不能在项目运行中动态修改任务执行时间,实在不太灵活。
    的头像 发表于 09-30 11:16 1494次阅读

    SpringBoot常用注解及使用方法1

    基于 SpringBoot 平台开发的项目数不胜数,与常规的基于`Spring`开发的项目最大的不同之处,SpringBoot 里面提供了大量的注解用于快速开发,而且非常简单,基本可以
    的头像 发表于 04-07 11:51 445次阅读

    如何在SpringBoot中解决Redis的缓存穿透等问题

    今天给大家介绍一下如何在SpringBoot中解决Redis的缓存穿透、缓存击穿、缓存雪崩的问题。
    的头像 发表于 04-28 11:35 523次阅读

    如何正确使用SpringBoot项目中缓存Cache

    缓存可以通过将经常访问的数据存储在内存中,减少底层数据源如数据库的压力,从而有效提高系统的性能和稳定性。我想大家的项目中或多或少都有使用过,我们项目也不例外,但是最近在review公司的代码的时候写的很蠢且low, 大致写法如下
    的头像 发表于 05-11 11:01 1042次阅读
    如何<b class='flag-5'>正确</b>使用<b class='flag-5'>SpringBoot</b><b class='flag-5'>项目中</b><b class='flag-5'>缓存</b>Cache

    什么是springBoot业务组件化开发?谈谈SpringBoot业务组件化

    首先,谈一谈什么是“springBoot业务组件化开发”,最近一直在开发一直面临这一个问题,就是相同的业务场景场景在一个项目中使用了,又需要再另外一个项目中复用,一遍又一遍的复制代码,然后想将该业务的代码在不同的
    的头像 发表于 07-20 11:30 586次阅读
    什么是<b class='flag-5'>springBoot</b>业务组件化开发?谈谈<b class='flag-5'>SpringBoot</b>业务组件化

    如何在Rust项目中使用InfluxDB 2.x

    了更好的性能和更好的用户体验。Rust语言提供了InfluxDB 2.x的官方客户端库,可以方便地在Rust项目中使用InfluxDB 2.x。 本教程将介绍如何在Rust项目中使用InfluxDB
    的头像 发表于 09-19 16:33 355次阅读