前言

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

public User getById(String id) {
User user = cache.getUser();
if(user != null) {
return user;
}
// 从数据库获取
user = loadFromDB(id);
cahce.put(id, user);
return user;
}

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

欢迎关注个人公众号【JAVA旭阳】交流学习!

启用缓存@EnableCaching

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

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

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

自定义缓存管理器

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

  1. 添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
  1. 配置Redis缓存管理器
@Configuration
@EnableCaching
public class CacheConfig { @Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory();
} @Bean
public CacheManager cacheManager() {
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.disableCachingNullValues()
.serializeValuesWith(SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())); RedisCacheManager redisCacheManager = RedisCacheManager.builder(redisConnectionFactory())
.cacheDefaults(redisCacheConfiguration)
.build(); return redisCacheManager;
}
}

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

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

@Cacheable

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

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

@Service
public class MyService { @Autowired
private MyRepository repository; @Cacheable(value = "myCache", key = "#id")
public MyEntity getEntityById(Long id) {
return repository.findById(id).orElse(null);
}
}

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

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

@CachePut

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

@Service
public class MyService { @Autowired
private MyRepository repository; @CachePut(value = "myCache", key = "#entity.id")
public void saveEntity(MyEntity entity) {
repository.save(entity);
}
}

@CacheEvict

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

@Service
public class MyService { @Autowired
private MyRepository repository; @CacheEvict(value = "myCache", key = "#id")
public void deleteEntityById(Long id) {
repository.deleteById(id);
}
}

@Caching

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

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

@Service
public class MyService { @Autowired
private MyRepository repository; @Cacheable(value = "myCache", key = "#id")
public MyEntity getEntityById(Long id) {
return repository.findById(id).orElse(null);
} @Caching(evict = {
@CacheEvict(value = "myCache", key = "#entity.id"),
@CacheEvict(value = "otherCache", key = "#entity.id")
})
public void saveEntity(MyEntity entity) {
repository.save(entity);
} }

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

@Service
public class MyService { @Caching(
cacheable = {
@Cacheable(value = "myCache", key = "#id")
},
evict = {
@CacheEvict(value = "otherCache", key = "#id")
}
)
public MyEntity getEntityById(Long id) {
return repository.findById(id).orElse(null);
} }

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

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

@Service
public class MyService { @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")
}
)
public MyEntity saveData(Long id, String name) {
// Code to save data to a database or external API
MyEntity entity = new MyEntity(id, name);
return entity;
} }

@CacheConfig

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

@CacheConfig(cacheNames={"myCache"})
@Service
public class MyService { @Autowired
private MyRepository repository; @Cacheable(key = "#id")
public MyEntity getEntityById(Long id) {
return repository.findById(id).orElse(null);
} @CachePut(key = "#entity.id")
public void saveEntity(MyEntity entity) {
repository.save(entity);
} @CacheEvict(key = "#id")
public void deleteEntityById(Long id) {
repository.deleteById(id);
}
}

Condition & Unless

  • condition作用:指定缓存的条件(满足什么条件才缓存),可用 SpEL 表达式(如 #id>0,表示当入参 id 大于 0 时才缓存)
  • unless作用 : 否定缓存,即满足 unless 指定的条件时,方法的结果不进行缓存,使用 unless 时可以在调用的方法获取到结果之后再进行判断(如 #result == null,表示如果结果为 null 时不缓存)
//when id >10, the @CachePut works.
@CachePut(key = "#entity.id", condition="#entity.id > 10")
public void saveEntity(MyEntity entity) {
repository.save(entity);
} //when result != null, the @CachePut works.
@CachePut(key = "#id", condition="#result == null")
public void saveEntity1(MyEntity entity) {
repository.save(entity);
}

清理全部缓存

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

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

SpEL表达式

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

SpEL 表达式的语法

Spring Cache可用的变量

最佳实践

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

  • 只缓存经常读取的数据:缓存可以显着提高性能,但只缓存经常访问的数据很重要。很少或从不访问的缓存数据会占用宝贵的内存资源,从而导致性能问题。
  • 根据应用程序的特定需求选择合适的缓存提供程序和策略。SpringBoot 支持多种缓存提供程序,包括 EhcacheHazelcastRedis
  • 使用缓存时请注意潜在的线程安全问题。对缓存的并发访问可能会导致数据不一致或不正确,因此选择线程安全的缓存提供程序并在必要时使用适当的同步机制非常重要。
  • 避免过度缓存。缓存对于提高性能很有用,但过多的缓存实际上会消耗宝贵的内存资源,从而损害性能。在缓存频繁使用的数据和允许垃圾收集不常用的数据之间取得平衡很重要。
  • 使用适当的缓存逐出策略。使用缓存时,重要的是定义适当的缓存逐出策略以确保在必要时从缓存中删除旧的或陈旧的数据。
  • 使用适当的缓存键设计。缓存键对于每个数据项都应该是唯一的,并且应该考虑可能影响缓存数据的任何相关参数,例如用户 ID、时间或位置。
  • 常规数据(读多写少、即时性与一致性要求不高的数据)完全可以使用 Spring Cache,至于写模式下缓存数据一致性问题的解决,只要缓存数据有设置过期时间就足够了。
  • 特殊数据(读多写多、即时性与一致性要求非常高的数据),不能使用 Spring Cache,建议考虑特殊的设计(例如使用 Cancal 中间件等)。

欢迎关注个人公众号【JAVA旭阳】交流学习!

SpringBoot项目中使用缓存Cache的正确姿势!!!的更多相关文章

  1. Spring-Boot项目中配置redis注解缓存

    Spring-Boot项目中配置redis注解缓存 在pom中添加redis缓存支持依赖 <dependency> <groupId>org.springframework.b ...

  2. java项目中ehcache缓存最简单用法

      java项目中ehcache缓存最简单用法: 1.下载ehcache-core-2.4.3.jar复制到项目的lib目录下 2.新建ehcache.xml文件,放置在项目src目录下的resour ...

  3. springboot项目中接口入参的简单校验

    .katex { display: block; text-align: center; white-space: nowrap; } .katex-display > .katex > ...

  4. Springboot项目中 前端展示本地图片

    Springboot项目中 前端展示本地图片 本文使用的是Springboot官方推荐的thymeleaf(一种页面模板技术) 首先在pom文件加依赖 <dependency> <g ...

  5. 国际化的实现i18n--错误码国际化以及在springboot项目中使用

    国际化 ,英文叫 internationalization 单词太长 ,又被简称为 i18n(取头取尾中间有18个字母); 主要涉及3个类: Locale用来设置定制的语言和国家代码 Resource ...

  6. 谈谈MVC项目中的缓存功能设计的相关问题

    本文收集一些关于项目中为什么需要使用缓存功能,以及怎么使用等,在实际开发中对缓存的设计的考虑 为什么需要讨论缓存呢? 缓存是一个中大型系统所必须考虑的问题.为了避免每次请求都去访问后台的资源(例如数据 ...

  7. SpringBoot12 QueryDSL01之QueryDSL介绍、springBoot项目中集成QueryDSL

    1 QueryDSL介绍 1.1 背景 QueryDSL的诞生解决了HQL查询类型安全方面的缺陷:HQL查询的扩展需要用字符串拼接的方式进行,这往往会导致代码的阅读困难:通过字符串对域类型和属性的不安 ...

  8. 在SpringBoot项目中添加logback的MDC

    在SpringBoot项目中添加logback的MDC     先看下MDC是什么 Mapped Diagnostic Context,用于打LOG时跟踪一个“会话“.一个”事务“.举例,有一个web ...

  9. 自身使用的springboot项目中比较全的pom.xml

    在学习的时候常建新的项目,mark下商用的jar <dependency> <groupId>org.mybatis</groupId> <artifactI ...

  10. springboot 项目中获取默认注入的序列化对象 ObjectMapper

    在 springboot 项目中使用 @SpringBootApplication 会自动标记 @EnableAutoConfiguration 在接口中经常需要使用时间类型,Date ,如果想要格式 ...

随机推荐

  1. Nginx 代理解决跨域问题分析

    Nginx 代理解决跨域问题分析   当你遇到跨域问题,不要立刻就选择复制去尝试.请详细看完这篇文章再处理 .我相信它能帮到你. 分析前准备: 前端网站地址:http://localhost:8080 ...

  2. vue多图片上传组件

    <template> <!-- 上传控件 用法: <upload-widget v-model="imgUrl"></upload-widget ...

  3. Python 爬虫代码应该怎么写?

    对于入行已久的老程序员也并不一定精通爬虫代码,这些需要时间的沉淀还需要更多的实战案例,简单的问句你真的会写爬虫么?下面就是我日常写的一个y文件加上几个请求并且把需要的功能全部实现模块化,可以让我们爬虫 ...

  4. echarts属性大全

    // 全图默认背景  // backgroundColor: 'rgba(0,0,0,0)', // 默认色板 color: ['#ff7f50','#87cefa','#da70d6','#32cd ...

  5. 如何让charles无论怎么请求都返回一个结果

    1. map Local         将匹配的url映射到本地文件.这个需要首先将url右键,save Response,将原有报文保存到本地,然后映射到该文件,修改该文件即可,直接自己写费事2. ...

  6. 01 docker容器技术基础入门

    本章内容: 1.container是什么? 2.LXC技术介绍 3.namespaces-名称空间,实现资源隔离 4.容器的资源分配--Cgroup,实现资源分配 5.LXC与dockers ---- ...

  7. python 连接蓝牙设备并接收数据

    python 连接蓝牙设备 原始内容 # %% from binascii import hexlify import struct from bluepy.btle import Scanner, ...

  8. Word 给公式添加题注解决交叉引用中包含公式

    简记:回车,然后 Ctrl +Alt+Enter https://blog.csdn.net/wsj_jerry521/article/details/115163456

  9. 6. 基础查(会员信息) - 创建查询Web Api - 配置Table Permission

    ​ Power Portal中的Web API可以对门户页面中所有的Microsoft Dataverse实体进行创建.更新和删除操作.我们可以直接使用门户Web API对产品创建新客户.更新联系人或 ...

  10. [转]C++回调函数

    回调函数 回调函数是一个时时听到的概念,比如在windows API编程时遇到的WinProc函数,就是我们编写而由操作系统调用的函数.现在,我们需要慢慢又详细的记录一下这个问题. 库与使用者的问题 ...