使用spring cache让我的接口性能瞬间提升了
ztj100 2024-12-03 20:00 23 浏览 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。
- 基本用法
- 项目中如何使用
- 工作原理
一、基本用法
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
二、项目中如何使用
- 引入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先介绍到这里,如果这篇文档对您有所帮助或者有所启发的话,麻烦关注一下:苏三说技术,或者帮忙点赞或转发,坚持原创不易,您的支持是我坚持最大的动力。后面我会分享更多更实用的干货,谢谢大家的支持。
相关推荐
- 爬取电影视频数据(电影资源爬虫)
-
本文的文字及图片来源于网络,仅供学习、交流使用,不具有任何商业用途,如有问题请及时联系我们以作处理。作者:yangrq1018原文链接:https://segmentfault.com/a/11900...
- Python效率倍增的10个实用代码片段
-
引言Python是一门功能强大且灵活的编程语言,广泛应用于数据分析、Web开发、人工智能等多个领域。它的简洁语法和高可读性让开发者能够快速上手,但在实际工作中,我们常常会遇到一些重复性或繁琐的任务。这...
- Python数据处理:深入理解序列化与反序列化
-
在现代编程实践中,数据的序列化与反序列化是数据持久化、网络通信等领域不可或缺的技术。本文将深入探讨Python中数据序列化与反序列化的概念、实现方式以及数据验证的重要性,并提供丰富的代码示例。...
- 亿纬锂能:拟向PKL买地,在马来西亚建立锂电池制造厂
-
亿纬锂能5月12日公告,亿纬马来西亚与PEMAJUKELANGLAMASDN.BHD.(PKL)签订《MEMORANDUMOFUNDERSTANDING》(谅解备忘录),亿纬马来西亚拟向PKL购买标的...
- 一个超强的机器学习库(spark机器学习库)
-
简介PyCaret...
- 30天学会Python编程:9. Python文件与IO操作
-
9.1文件操作基础9.1.1文件操作流程9.1.2文件打开模式表9-1Python文件打开模式...
- Python的Pickle序列化与反序列化(python反序列化json)
-
动动小手,点击关注...
- python进阶突破内置模块——数据序列化与格式
-
数据序列化是将数据结构或对象转换为可存储/传输格式的过程,反序列化则是逆向操作。Python提供了多种工具来处理不同场景下的序列化需求。一、核心内置模块...
- 微信聊天记录可视化工具详细介绍(微信聊天记录分析报告小程序)
-
功能概要能做什么...
- Python常用文件操作库使用详解(python中文件操作的相关函数有哪些)
-
Python生态系统提供了丰富的文件操作库,可以处理各种复杂的文件操作需求。本教程将介绍Python中最常用的文件操作库及其实际应用。一、标准库核心模块1.1os模块-操作系统接口主要功能...
- Vue3+Django4全新技术实战全栈项目(已完结)
-
获课》aixuetang.xyz/5739/Django与推荐算法的集成及模型部署实践...
- 性能调优方面,经常要优化跑的最慢的代码,教你一种快速的方法
-
在我们遇到性能问题的时候,很多时候需要去查看性能的瓶颈在哪里,本篇文章就是提供了多种常用的方案来监控函数的运行时间。1.time首先说明,time模块很多是系统相关的,在不同的OS中可能会有一些精度差...
- Python解决读取excel数据慢的问题
-
前言:在做自动化测试的时候,我思考了一个问题,就是如果我们的测试用例随着项目的推进越来越多时,我们做自动化回归的时间也就越来越长,其中影响自动化测试速度的一个原因就是测试用例的读取问题。用例越多,所消...
- 【Python机器学习系列】基于Flask来构建API调用机器学习模型服务
-
这是我的第364篇...
- 不会用mmdet工具?速看MMDetection工具的终极指南
-
来源:计算机视觉工坊添加微信:dddvisiona,备注:目标检测,拉你入群。文末附行业细分群...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- idea eval reset (50)
- vue dispatch (70)
- update canceled (42)
- order by asc (53)
- spring gateway (67)
- 简单代码编程 贪吃蛇 (40)
- transforms.resize (33)
- redisson trylock (35)
- 卸载node (35)
- np.reshape (33)
- torch.arange (34)
- npm 源 (35)
- vue3 deep (35)
- win10 ssh (35)
- vue foreach (34)
- idea设置编码为utf8 (35)
- vue 数组添加元素 (34)
- std find (34)
- tablefield注解用途 (35)
- python str转json (34)
- java websocket客户端 (34)
- tensor.view (34)
- java jackson (34)
- vmware17pro最新密钥 (34)
- mysql单表最大数据量 (35)