筆者之前做商城項目時,做過商城首頁的商品分類功能。當時考慮分類是放在商城首頁,以後流量大,而且不經常變動,為了提升首頁訪問速度,我考慮使用緩存。對於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註解是緩存的開關,如果要使用緩存功能,就必要打開這個開關,這個註解可以定義在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")@Servicepublic 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@EnableCachingpublic class CacheConfig { @Bean public CacheManager cacheManager(){ CaffeineCacheManager cacheManager = new CaffeineCacheManager(); Caffeine<Object, Object> caffeine = Caffeine.newBuilder() .expireAfterWrite(10, TimeUnit.SECONDS) .maximumSize(1000); cacheManager.setCaffeine(caffeine); return cacheManager; }}3.使用Cacheable註解獲取數據
@Servicepublic class CategoryService { @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 {
boolean proxyTargetClass() default false; 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; 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)); } } 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)); } } 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)); } }
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,它最終會調到這個方法:
@Nullableprivate Object execute(final CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) { 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) { throw (CacheOperationInvoker.ThrowableWrapper) ex.getCause(); } } else { return invokeOperation(invoker); } }
processCacheEvicts(contexts.get(CacheEvictOperation.class), true, CacheOperationExpressionEvaluator.NO_RESULT);
Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class));
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)) { cacheValue = cacheHit.get(); returnValue = wrapCacheValue(method, cacheValue); } else { returnValue = invokeOperation(invoker); cacheValue = unwrapReturnValue(returnValue); }
collectPutRequests(contexts.get(CachePutOperation.class), cacheValue, cachePutRequests);
for (CachePutRequest cachePutRequest : cachePutRequests) { cachePutRequest.apply(cacheValue); }
processCacheEvicts(contexts.get(CacheEvictOperation.class), false, cacheValue);
return returnValue;}也行有些朋友看到這裡會有一個疑問:
既然spring cache的增刪改查都有了,為啥還要 @Caching 註解呢?
其實是這樣的:spring考慮如果除了增刪改查之外,如果用戶需要自定義自己的註解,或者有些比較複雜的功能需要增刪改查的情況,這時就可以用@Caching 註解來實現。
還要一個問題:
上面的例子中使用的緩存key是#type,但是如果有些緩存key比較複雜,是實體中的幾個欄位組成的,這種情況要如何定義呢?
一起看看下面的例子:
@Datapublic 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實例
@Beanpublic CategoryGenerator categoryGenerator() { return new CategoryGenerator();}3.修改之前定義的key
@Cacheable(value = "category", key = "categoryGenerator")public CategoryModel getCategory2(QueryCategoryModel queryCategoryModel) { return getCategoryByType(queryCategoryModel.getType());}