硬核 | 使用spring cache让我的接口性能瞬间提升了100倍

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 Tair(兼容Redis),内存型 2GB
简介: 使用spring cache让我的接口性能瞬间提升了100倍

前言


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


优化前系统流程图:

333.png


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

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


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


优化后的系统流程图:

222.png

我们看到,其他的流程都没有变,只是在获取分类接口中增加了先从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);
    }
}

在浏览器中调用接口:

216.png

可以看到,有数据返回。

再看看控制台的打印。

215.png

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

然后再重新请求一次,

214.png

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

213.png

说明缓存生效了,下面我介绍一下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方法

 
         

该方法引入了AutoProxyRegistrarProxyCachingConfiguration两个类


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


重点看看ProxyCachingConfiguration

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);
  }


@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内部定义的。

217.png


另外定义了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先介绍到这里

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
11天前
|
XML Java 数据格式
探索Spring之利剑:ApplicationContext接口
本文深入介绍了Spring框架中的核心接口ApplicationContext,解释了其作为应用容器的功能,包括事件发布、国际化支持等,并通过基于XML和注解的配置示例展示了如何使用ApplicationContext管理Bean实例。
40 6
|
2月前
|
存储 安全 Java
|
2月前
|
自然语言处理 JavaScript Java
Spring 实现 3 种异步流式接口,干掉接口超时烦恼
本文介绍了处理耗时接口的几种异步流式技术,包括 `ResponseBodyEmitter`、`SseEmitter` 和 `StreamingResponseBody`。这些工具可在执行耗时操作时不断向客户端响应处理结果,提升用户体验和系统性能。`ResponseBodyEmitter` 适用于动态生成内容场景,如文件上传进度;`SseEmitter` 用于实时消息推送,如状态更新;`StreamingResponseBody` 则适合大数据量传输,避免内存溢出。文中提供了具体示例和 GitHub 地址,帮助读者更好地理解和应用这些技术。
405 0
|
3月前
|
存储 数据采集 Java
Spring Boot 3 实现GZIP压缩优化:显著减少接口流量消耗!
在Web开发过程中,随着应用规模的扩大和用户量的增长,接口流量的消耗成为了一个不容忽视的问题。为了提升应用的性能和用户体验,减少带宽占用,数据压缩成为了一个重要的优化手段。在Spring Boot 3中,通过集成GZIP压缩技术,我们可以显著减少接口流量的消耗,从而优化应用的性能。本文将详细介绍如何在Spring Boot 3中实现GZIP压缩优化。
449 6
|
2月前
|
存储 NoSQL Java
Spring Boot项目中使用Redis实现接口幂等性的方案
通过上述方法,可以有效地在Spring Boot项目中利用Redis实现接口幂等性,既保证了接口操作的安全性,又提高了系统的可靠性。
58 0
|
4月前
|
缓存 NoSQL Java
SpringBoot的三种缓存技术(Spring Cache、Layering Cache 框架、Alibaba JetCache 框架)
Spring Cache 是 Spring 提供的简易缓存方案,支持本地与 Redis 缓存。通过添加 `spring-boot-starter-data-redis` 和 `spring-boot-starter-cache` 依赖,并使用 `@EnableCaching` 开启缓存功能。JetCache 由阿里开源,功能更丰富,支持多级缓存和异步 API,通过引入 `jetcache-starter-redis` 依赖并配置 YAML 文件启用。Layering Cache 则提供分层缓存机制,需引入 `layering-cache-starter` 依赖并使用特定注解实现缓存逻辑。
1211 1
SpringBoot的三种缓存技术(Spring Cache、Layering Cache 框架、Alibaba JetCache 框架)
|
4月前
|
Prometheus 监控 Cloud Native
Spring Boot 性能护航!Prometheus、Grafana、ELK 组合拳,点燃数字化时代应用稳定之火
【8月更文挑战第29天】在现代软件开发中,保证应用性能与稳定至关重要。Spring Boot 作为流行的 Java 框架,结合 Prometheus、Grafana 和 ELK 可显著提升监控与分析能力。Prometheus 负责收集时间序列数据,Grafana 将数据可视化,而 ELK (Elasticsearch、Logstash、Kibana)则管理并分析应用日志。通过具体实例演示了如何在 Spring Boot 应用中集成这些工具:配置 Prometheus 获取度量信息、Grafana 显示结果及 ELK 分析日志,从而帮助开发者快速定位问题,确保应用稳定高效运行。
125 1
|
4月前
|
JSON 安全 Java
|
4月前
|
存储 SQL Java
|
5月前
|
SQL Java 数据库
实时计算 Flink版产品使用问题之Spring Boot集成Flink可以通过什么方式实现通过接口启动和关闭Flink程序
实时计算Flink版作为一种强大的流处理和批处理统一的计算框架,广泛应用于各种需要实时数据处理和分析的场景。实时计算Flink版通常结合SQL接口、DataStream API、以及与上下游数据源和存储系统的丰富连接器,提供了一套全面的解决方案,以应对各种实时计算需求。其低延迟、高吞吐、容错性强的特点,使其成为众多企业和组织实时数据处理首选的技术平台。以下是实时计算Flink版的一些典型使用合集。