百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术分类 > 正文

使用spring cache让我的接口性能瞬间提升了

ztj100 2024-12-03 20:00 18 浏览 0 评论

笔者之前做商城项目时,做过商城首页的商品分类功能。当时考虑分类是放在商城首页,以后流量大,而且不经常变动,为了提升首页访问速度,我考虑使用缓存。对于java开发而言,首先的缓存当然是redis。

优化前系统流程图:

我们从图中可以看到,分类功能分为生成分类数据 和 获取分类数据两个流程,生成分类数据流程是有个JOB每隔5分钟执行一次,从mysql中获取分类数据封装成首页需要展示的分类数据结构,然后保存到redis中。获取分类数据流程是商城首页调用分类接口,接口先从redis中获取数据,如果没有获取到再从mysql中获取。

一般情况下从redis就都能获取数据,因为相应的key是没有设置过期时间的,数据会一直都存在。以防万一,我们做了一次兜底,如果获取不到数据,就会从mysql中获取。

本以为万事大吉,后来,在系统上线之前,测试对商城首页做了一次性能压测,发现qps是100多,一直上不去。我们仔细分析了一下原因,发现了两个主要的优化点:去掉多余的接口日志打印 和 分类接口引入redis cache做一次二级缓存。日志打印我在这里就不多说了,不是本文的重点,我们重点说一下redis cache。

优化后的系统流程图:

我们看到,其他的流程都没有变,只是在获取分类接口中增加了先从spring cache中获取分类数据的功能,如果获取不到再从redis中获取,再获取不到才从mysql中获取。

经过这样一次小小的调整,再重新压测接口,性能一下子提升了N倍,满足了业务要求。如此美妙的一次优化经验,有必要跟大家分析一下。

我将从以下几个方面给大家分享一下spring cache。

  1. 基本用法
  2. 项目中如何使用
  3. 工作原理

一、基本用法

SpringCache缓存功能的实现是依靠下面的这几个注解完成的。

  • @EnableCaching:开启缓存功能
  • @Cacheable:获取缓存
  • @CachePut:更新缓存
  • @CacheEvict:删除缓存
  • @Caching:组合定义多种缓存功能
  • @CacheConfig:定义公共设置,位于类之上

@EnableCaching注解是缓存的开关,如果要使用缓存功能,就必要打开这个开关,这个注解可以定义在Configuration类或者springboot的启动类上面。

@Cacheable、@CachePut、@CacheEvict 这三个注解的用户差不多,定义在需要缓存的具体类或方法上面。

@Cacheable(key="'id:'+#id")

public User getUser(int id) {

return userService.getUserById(id);

}

@CachePut(key="'id:'+#user.id")

public User insertUser(User user) {

userService.insertUser(user);

return user;

}

@CacheEvict(key="'id:'+#id")

public int deleteUserById(int id) {

userService.deleteUserById(id);

return id;

}

需要注意的是@Caching注解跟另外三个注解不同,它可以组合另外三种注解,自定义新注解。

@Caching(

cacheable = {@Cacheable(/*value = "emp",*/key = "#lastName")

put = {@CachePut(/*value = "emp",*/key = "#result.id")}

)

public Employee getEmpByLastName(String lastName){

return employeeMapper.getEmpByLastName(lastName);

}

@CacheConfig一般定义在配置类上面,可以抽取缓存的公共配置,可以定义这个类全局的缓存名称,其他的缓存方法就可以不配置缓存名称了。

@CacheConfig(cacheNames = "emp")

@Service

public class EmployeeService

二、项目中如何使用

  1. 引入caffeine的相关jar包我们这里使用caffeine,而非guava,因为Spring Boot 2.0中取代了guava

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-cache</artifactId>

</dependency>

<dependency>

<groupId>com.github.ben-manes.caffeine</groupId>

<artifactId>caffeine</artifactId>

<version>2.6.0</version>

</dependency>

2. 配置CacheManager,开启EnableCaching

@Configuration

@EnableCaching

public class CacheConfig {

@Bean

public CacheManager cacheManager(){

CaffeineCacheManager cacheManager = new CaffeineCacheManager();

//Caffeine配置

Caffeine<Object, Object> caffeine = Caffeine.newBuilder()

//最后一次写入后经过固定时间过期

.expireAfterWrite(10, TimeUnit.SECONDS)

//缓存的最大条数

.maximumSize(1000);

cacheManager.setCaffeine(caffeine);

return cacheManager;

}

}

3.使用Cacheable注解获取数据

@Service

public class CategoryService {

//category是缓存名称,#type是具体的key,可支持el表达式

@Cacheable(value = "category", key = "#type")

public CategoryModel getCategory(Integer type) {

return getCategoryByType(type);

}

private CategoryModel getCategoryByType(Integer type) {

System.out.println("根据不同的type:" + type + "获取不同的分类数据");

CategoryModel categoryModel = new CategoryModel();

categoryModel.setId(1L);

categoryModel.setParentId(0L);

categoryModel.setName("电器");

categoryModel.setLevel(3);

return categoryModel;

}

}

4.测试

@Api(tags = "category", description = "分类相关接口")

@RestController

@RequestMapping("/category")

public class CategoryController {

@Autowired

private CategoryService categoryService;

@GetMapping("/getCategory")

public CategoryModel getCategory(@RequestParam("type") Integer type) {

return categoryService.getCategory(type);

}

}

在浏览器中调用接口:

可以看到,有数据返回。

再看看控制台的打印。

有数据打印,说明第一次请求进入了categoryService.getCategory方法的内部。

然后再重新请求一次,

还是有数据,返回。但是控制台没有重新打印新数据,还是以前的数据,说明这一次请求走的是缓存,没有进入categoryService.getCategory方法的内部。在5分钟以内,再重复请求该接口,一直都是直接从缓存中获取数据。

说明缓存生效了,下面我介绍一下spring cache的工作原理

三、工作原理

通过上面的例子,相当朋友们对spring cache在项目中的用法有了一定的认识。那么它的工作原理是什么呢?

相信聪明的朋友们,肯定会想到,它用了AOP

没错,它就是用了AOP。那么具体是怎么用的?

我们先看看EnableCaching注解

@Target(ElementType.TYPE)

@Retention(RetentionPolicy.RUNTIME)

@Documented

@Import(CachingConfigurationSelector.class)

public @interface EnableCaching {

// false JDK动态代理 true cglib代理

boolean proxyTargetClass() default false;

//通知模式 JDK动态代理 或 AspectJ

AdviceMode mode() default AdviceMode.PROXY;

//排序

int order() default Ordered.LOWEST_PRECEDENCE;

}

这个数据很简单,定义了代理相关参数,引入了CachingConfigurationSelector类。再看看该类的getProxyImports方法

private String[] getProxyImports() {

List<String> result = new ArrayList<>(3);

result.add(AutoProxyRegistrar.class.getName());

result.add(ProxyCachingConfiguration.class.getName());

if (jsr107Present && jcacheImplPresent) {

result.add(PROXY_JCACHE_CONFIGURATION_CLASS);

}

return StringUtils.toStringArray(result);

}

该方法引入了AutoProxyRegistrar和ProxyCachingConfiguration两个类

AutoProxyRegistrar是让spring cache拥有AOP的能力(至于如何拥有AOP的能力,这个是单独的话题,感兴趣的朋友可以自己阅读一下源码。或者关注一下我的公众账号,后面会有专门AOP的专题)。

重点看看ProxyCachingConfiguration

@Configuration

@Role(BeanDefinition.ROLE_INFRASTRUCTURE)

public class ProxyCachingConfiguration extends AbstractCachingConfiguration {

@Bean(name = CacheManagementConfigUtils.CACHE_ADVISOR_BEAN_NAME)

@Role(BeanDefinition.ROLE_INFRASTRUCTURE)

public BeanFactoryCacheOperationSourceAdvisor cacheAdvisor() {

BeanFactoryCacheOperationSourceAdvisor advisor = new BeanFactoryCacheOperationSourceAdvisor();

advisor.setCacheOperationSource(cacheOperationSource());

advisor.setAdvice(cacheInterceptor());

if (this.enableCaching != null) {

advisor.setOrder(this.enableCaching.<Integer>getNumber("order"));

}

return advisor;

}

@Bean

@Role(BeanDefinition.ROLE_INFRASTRUCTURE)

public CacheOperationSource cacheOperationSource() {

return new AnnotationCacheOperationSource();

}

@Bean

@Role(BeanDefinition.ROLE_INFRASTRUCTURE)

public CacheInterceptor cacheInterceptor() {

CacheInterceptor interceptor = new CacheInterceptor();

interceptor.setCacheOperationSources(cacheOperationSource());

if (this.cacheResolver != null) {

interceptor.setCacheResolver(this.cacheResolver);

}

else if (this.cacheManager != null) {

interceptor.setCacheManager(this.cacheManager);

}

if (this.keyGenerator != null) {

interceptor.setKeyGenerator(this.keyGenerator);

}

if (this.errorHandler != null) {

interceptor.setErrorHandler(this.errorHandler);

}

return interceptor;

}

}

哈哈哈,这个类里面定义了AOP的三大要素:advisor、interceptor和Pointcut,只是Pointcut是在BeanFactoryCacheOperationSourceAdvisor内部定义的。

另外定义了CacheOperationSource类,该类封装了cache方法签名注解的解析工作,形成CacheOperation的集合。它的构造方法会实例化SpringCacheAnnotationParser,现在看看这个类的parseCacheAnnotations方法。

private Collection<CacheOperation> parseCacheAnnotations(

DefaultCacheConfig cachingConfig, AnnotatedElement ae, boolean localOnly) {

Collection<CacheOperation> ops = null;

//找@cacheable注解方法

Collection<Cacheable> cacheables = (localOnly ? AnnotatedElementUtils.getAllMergedAnnotations(ae, Cacheable.class) :

AnnotatedElementUtils.findAllMergedAnnotations(ae, Cacheable.class));

if (!cacheables.isEmpty()) {

ops = lazyInit(null);

for (Cacheable cacheable : cacheables) {

ops.add(parseCacheableAnnotation(ae, cachingConfig, cacheable));

}

}

//找@cacheEvict注解的方法

Collection<CacheEvict> evicts = (localOnly ? AnnotatedElementUtils.getAllMergedAnnotations(ae, CacheEvict.class) :

AnnotatedElementUtils.findAllMergedAnnotations(ae, CacheEvict.class));

if (!evicts.isEmpty()) {

ops = lazyInit(ops);

for (CacheEvict evict : evicts) {

ops.add(parseEvictAnnotation(ae, cachingConfig, evict));

}

}

//找@cachePut注解的方法

Collection<CachePut> puts = (localOnly ? AnnotatedElementUtils.getAllMergedAnnotations(ae, CachePut.class) :

AnnotatedElementUtils.findAllMergedAnnotations(ae, CachePut.class));

if (!puts.isEmpty()) {

ops = lazyInit(ops);

for (CachePut put : puts) {

ops.add(parsePutAnnotation(ae, cachingConfig, put));

}

}

//找@Caching注解的方法

Collection<Caching> cachings = (localOnly ? AnnotatedElementUtils.getAllMergedAnnotations(ae, Caching.class) :

AnnotatedElementUtils.findAllMergedAnnotations(ae, Caching.class));

if (!cachings.isEmpty()) {

ops = lazyInit(ops);

for (Caching caching : cachings) {

Collection<CacheOperation> cachingOps = parseCachingAnnotation(ae, cachingConfig, caching);

if (cachingOps != null) {

ops.addAll(cachingOps);

}

}

}

return ops;

}

我们看到这个类会解析@cacheable、@cacheEvict、@cachePut 和 @Caching注解的参数,封装到CacheOperation集合中。

此外,spring cache 功能的关键就是上面的拦截器:CacheInterceptor,它最终会调到这个方法:

@Nullable

private Object execute(final CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) {

// Special handling of synchronized invocation

if (contexts.isSynchronized()) {

CacheOperationContext context = contexts.get(CacheableOperation.class).iterator().next();

if (isConditionPassing(context, CacheOperationExpressionEvaluator.NO_RESULT)) {

Object key = generateKey(context, CacheOperationExpressionEvaluator.NO_RESULT);

Cache cache = context.getCaches().iterator().next();

try {

return wrapCacheValue(method, cache.get(key, () -> unwrapReturnValue(invokeOperation(invoker))));

}

catch (Cache.ValueRetrievalException ex) {

// The invoker wraps any Throwable in a ThrowableWrapper instance so we

// can just make sure that one bubbles up the stack.

throw (CacheOperationInvoker.ThrowableWrapper) ex.getCause();

}

}

else {

// No caching required, only call the underlying method

return invokeOperation(invoker);

}

}

// 执行@CacheEvict的逻辑,这里是当beforeInvocation为true时清缓存

processCacheEvicts(contexts.get(CacheEvictOperation.class), true,

CacheOperationExpressionEvaluator.NO_RESULT);

// 获取命中的缓存对象

Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class));

//如果没有命中,则生成一个put的请求

List<CachePutRequest> cachePutRequests = new LinkedList<>();

if (cacheHit == null) {

collectPutRequests(contexts.get(CacheableOperation.class),

CacheOperationExpressionEvaluator.NO_RESULT, cachePutRequests);

}

Object cacheValue;

Object returnValue;

if (cacheHit != null && !hasCachePut(contexts)) {

// If there are no put requests, just use the cache hit

cacheValue = cacheHit.get();

returnValue = wrapCacheValue(method, cacheValue);

}

else {

// 如果没有获得缓存对象,则调用业务方法获得返回对象,这是关键代码

returnValue = invokeOperation(invoker);

cacheValue = unwrapReturnValue(returnValue);

}

// 收集@CachePuts数据

collectPutRequests(contexts.get(CachePutOperation.class), cacheValue, cachePutRequests);

// 执行cachePut或没有命中的Cacheable请求,将返回对象放到缓存中

for (CachePutRequest cachePutRequest : cachePutRequests) {

cachePutRequest.apply(cacheValue);

}

// 执行@CacheEvict的逻辑,这里是当beforeInvocation为false时清缓存

processCacheEvicts(contexts.get(CacheEvictOperation.class), false, cacheValue);

return returnValue;

}

也行有些朋友看到这里会有一个疑问:

既然spring cache的增删改查都有了,为啥还要 @Caching 注解呢?

其实是这样的:spring考虑如果除了增删改查之外,如果用户需要自定义自己的注解,或者有些比较复杂的功能需要增删改查的情况,这时就可以用@Caching 注解来实现。

还要一个问题:

上面的例子中使用的缓存key是#type,但是如果有些缓存key比较复杂,是实体中的几个字段组成的,这种情况要如何定义呢?

一起看看下面的例子:

@Data

public class QueryCategoryModel {

/**

* 系统编号

*/

private Long id;

/**

* 父分类编号

*/

private Long parentId;

/**

* 分类名称

*/

private String name;

/**

* 分类层级

*/

private Integer level;

/**

* 类型

*/

private Integer type;

}

@Cacheable(value = "category", key = "#type")

public CategoryModel getCategory2(QueryCategoryModel queryCategoryModel) {

return getCategoryByType(queryCategoryModel.getType());

}

这个例子中需要用到QueryCategoryModel实体对象的所有字段,作为一个key,这种情况要如何定义呢?

1.自定义一个类实现KeyGenerator接口

public class CategoryGenerator implements KeyGenerator {

@Override

public Object generate(Object target, Method method, Object... params) {

return target.getClass().getSimpleName() + "_"

+ method.getName() + "_"

+ StringUtils.arrayToDelimitedString(params, "_");

}

}

2.在CacheConfig类中定义CategoryGenerator的bean实例

@Bean

public CategoryGenerator categoryGenerator() {

return new CategoryGenerator();

}

3.修改之前定义的key

@Cacheable(value = "category", key = "categoryGenerator")

public CategoryModel getCategory2(QueryCategoryModel queryCategoryModel) {

return getCategoryByType(queryCategoryModel.getType());

}

好了,spring cache先介绍到这里,如果这篇文档对您有所帮助或者有所启发的话,麻烦关注一下:苏三说技术,或者帮忙点赞或转发,坚持原创不易,您的支持是我坚持最大的动力。后面我会分享更多更实用的干货,谢谢大家的支持。

相关推荐

电脑装系统用GHOST好,还是原装版本好?老司机都是这么装的

Hello大家好,我是兼容机之家的咖啡。安装Windows系统是原版ISO好还是ghost好呢?针对这个的问题,我们先来科普一下什么是ghost系统,和原版ISO镜像两者之间有哪些优缺点。如果是很了解...

苹果 iOS 14.5.1/iPadOS 14.5.1 正式版发布

IT之家5月4日消息今日凌晨,苹果发布了iOS14.5.1与iPadOS14.5.1正式版更新。这一更新距iOS14.5正式版发布过去了一周时间。IT之家了解到,苹果表示,...

iOS 13.1.3 正式版发布 包含错误修复和改进

苹果今天发布了iOS13.1.3和iPadOS13.1.3,这是iOS13发布之后第四个升级补丁。iOS13.1.2两周前发布。iOS13.1.3主要包括针对iPad和...

还不理解 Error 和 Exception 吗,看这篇就够了

在Java中的基本理念是结构不佳的代码不能运行,发现错误的理想时期是在编译期间,因为你不用运行程序,只是凭借着对Java基本理念的理解就能发现问题。但是编译期并不能找出所有的问题,有一些N...

Linux 开发人员发现了导致 MacBook“无法启动”的 macOS 错误

“多个严重”错误影响配备ProMotion显示屏的MacBookPro。...

启动系统时无法正常启动提示\windows\system32\winload.efi

启动系统时无法正常启动提示\windows\system32\winload.efi。该怎么解决?  最近有用户遇到了开机遇到的问题,是Windows未能启动。原因可能是最近更改了硬件或软件。虽然提...

离线部署之两种构建Ragflow镜像的方式,dify同理

在实际项目交付过程中,经常遇到要离线部署的问题,生产服务器无法连接外网,这时就需要先构建好ragflow镜像,然后再拷到U盘或刻盘,下面介绍两种构建ragflow镜像的方式。性能测试(网络情况好的情况...

Go语言 error 类型详解(go语言 异常)

Go语言的error类型是用于处理程序运行中错误情况的核心机制。它通过显式的返回值(而非异常抛出)来管理错误,强调代码的可控性和清晰性。以下是详细说明及示例:一、error类型的基本概念内置接口...

Mac上“闪烁的问号”错误提示如何修复?

现在Mac电脑的用户越来越多,Mac电脑在使用过程中也会出现系统故障。当苹果电脑无法找到系统软件时,Mac会给出一个“闪烁的问号”的标志。很多用户受到过闪烁问号这一常见的错误提示的影响,如何解决这个问...

python散装笔记——177 sys 模块(python sys模块详解)

sys模块提供了访问程序运行时环境的函数和值,例如命令行参数...

30天自制操作系统:第一天(30天自制操作系统电子书)

因为咱们的目的是为了研究操作系统的组成,所以直接从系统启动的第二阶段的主引导记录开始。前提是将编译工具放在该文件目录的同级目录下,该工具为日本人川合秀实自制的编译程序,优化过的nasm编译工具。...

五大原因建议您现在不要升级iOS 13或iPadOS

今天苹果放出了iPadOS和iOS13的公测版本,任何对新版功能感兴趣的用户都可以下载安装参与测试。除非你想要率先体验Dark模式,以及使用AppleID来登陆Facebook等服务,那么外媒CN...

Python安装包总报错?这篇解决指南让你告别pip烦恼!

在Python开发中,...

苹果提供了在M1 Mac上修复macOS重装错误的方案

#AppleM1芯片#在苹果新的M1Mac推出后不久,我们看到有报道称,在这些机器上恢复和重新安装macOS,可能会导致安装错误,使你的Mac无法使用。具体来说,错误信息如下:"An...

黑苹果卡代码篇三:常见卡代码问题,满满的干货

前言...

取消回复欢迎 发表评论: