The author has done the commodity classification function of the home page of the mall when doing the mall project before. At that time, it was considered that the classification was placed on the home page of the mall. In the future, the traffic was large and did not change frequently. In order to improve the access speed of the home page, I considered using cache. For java development, the first cache is redis.
System flow chart before optimization:

We can see from the figure that the classification function is divided into two processes: generating classification data and obtaining classification data. The process of generating classification data is that a JOB is executed every 5 minutes to obtain classification data from mysql, package it into the classification data structure to be displayed on the home page, and then save it in redis. The process of obtaining classified data is to call the classification interface on the front page of the mall. The interface first obtains data from redis, and then obtains data from MySQL if it is not obtained.
Generally, data can be obtained from redis, because the expiration time of the corresponding key is not set, and the data will always exist. Just in case, we did a thorough investigation. If we can't get the data, we will get it from mysql.
I thought everything would be fine. Later, before the system went online, the test conducted a one-time energy pressure test on the home page of the mall and found that qps was more than 100 and couldn't go up all the time. We carefully analyzed the reasons and found two main optimization points: remove the redundant interface log printing and classification interface, and introduce redis cache as a secondary cache. I won't talk more about log printing here. It's not the focus of this article. Let's focus on redis cache.
Optimized system flow chart:

We can see that other processes have not changed, but the function of obtaining classification data from spring cache is added to the obtain classification interface. If it cannot be obtained, it can be obtained from redis, and then from mysql.
After such a small adjustment, the pressure test interface is re tested, and the performance is suddenly improved by N times to meet the business requirements. Such a wonderful optimization experience, it is necessary to analyze it with you.
I will share spring cache with you from the following aspects.
- Basic Usage
- How to use in a project
- working principle
1, Basic usage
The implementation of spring cache caching function depends on the following annotations.
- @EnableCaching: enables caching
- @Cacheable: get cache
- @CachePut: update cache
- @CacheEvict: delete cache
- @Caching: multiple caching functions are defined in combination
- @CacheConfig: defines public settings, which are above the class
@The EnableCaching annotation is a cache switch. If you want to use the cache function, you need to turn this switch on. This annotation can be defined on the Configuration class or the startup class of springboot.
@The users of Cacheable, @ CachePut and @ CacheEvict annotations are similar, and are defined on the specific classes or methods that need to be cached.
@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; }
It should be noted that the @ Caching annotation is different from the other three annotations. It can combine the other three annotations to customize the new annotation.
@Caching( cacheable = {@Cacheable(/*value = "emp",*/key = "#lastName") put = {@CachePut(/*value = "emp",*/key = "#result.id")} ) public Employee getEmpByLastName(String lastName){ return employeeMapper.getEmpByLastName(lastName); }
@CacheConfig is generally defined on the configuration class. You can extract the public configuration of the cache. You can define the global cache name of this class. Other cache methods can not configure the cache name.
@CacheConfig(cacheNames = "emp") @Service public class EmployeeService
2, How to use in a project
- Introduce relevant jar packages of caffeine We use caffeine instead of guava here because guava is replaced in Spring Boot 2.0
<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. Configure CacheManager and enable EnableCaching
@Configuration @EnableCaching public class CacheConfig { @Bean public CacheManager cacheManager(){ CaffeineCacheManager cacheManager = new CaffeineCacheManager(); //Caffeine configuration Caffeine<Object, Object> caffeine = Caffeine.newBuilder() //Expires after a fixed time after the last write .expireAfterWrite(10, TimeUnit.SECONDS) //Maximum number of cache entries .maximumSize(1000); cacheManager.setCaffeine(caffeine); return cacheManager; } }
3. Use Cacheable annotation to obtain data
@Service public class CategoryService { //category is the cache name, #type is the specific key, and el expressions are supported @Cacheable(value = "category", key = "#type") public CategoryModel getCategory(Integer type) { return getCategoryByType(type); } private CategoryModel getCategoryByType(Integer type) { System.out.println("According to different type:" + type + "Get different classification data"); CategoryModel categoryModel = new CategoryModel(); categoryModel.setId(1L); categoryModel.setParentId(0L); categoryModel.setName("an electric appliance"); categoryModel.setLevel(3); return categoryModel; } }
4. Test
@Api(tags = "category", description = "Classification related interface") @RestController @RequestMapping("/category") public class CategoryController { @Autowired private CategoryService categoryService; @GetMapping("/getCategory") public CategoryModel getCategory(@RequestParam("type") Integer type) { return categoryService.getCategory(type); } }
Calling interface in browser:

As you can see, there is data returned.
Then look at the printing of the console.

There is data printing, indicating that the first request entered the categoryservice Inside the getcategory method.
And then ask again,

Still have data, return. However, the console did not reprint the new data or the previous data, indicating that the request went through the cache and did not enter the categoryservice Inside the getcategory method. Within 5 minutes, repeat the request for the interface, and always get data directly from the cache.

Note that the cache is effective. Let me introduce the working principle of spring cache
3, Working principle
Through the above example, quite a few friends have a certain understanding of the usage of spring cache in the project. So how does it work?
I believe smart friends will think that it uses AOP.
Yes, it uses AOP. So how does it work?
Let's look at the EnableCaching annotation first
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Import(CachingConfigurationSelector.class) public @interface EnableCaching { // false JDK dynamic proxy true cglib proxy boolean proxyTargetClass() default false; //Notification mode JDK dynamic proxy or AspectJ AdviceMode mode() default AdviceMode.PROXY; //sort int order() default Ordered.LOWEST_PRECEDENCE; }
This data is very simple. It defines the proxy related parameters and introduces the CachingConfigurationSelector class. Take another look at the getProxyImports method of this class
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); }
This method introduces two classes: autoproxyregister and ProxyCachingConfiguration
Autoproxyregister enables spring cache to have AOP capability (as for how to have AOP capability, this is a separate topic. Interested friends can read the source code by themselves. Or pay attention to my public account, and there will be a special AOP topic later).
Focus on 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; } }
Hahaha, this class defines three elements of AOP: advisor, interceptor and Pointcut, but the Pointcut is defined inside beanfactory cacheoperationsourceadvisor.

In addition, the CacheOperationSource class is defined, which encapsulates the parsing of cache method signature annotations to form a collection of cacheoperations. Its constructor will instantiate springcacheannotations parser. Now look at the parseCacheAnnotations method of this class.
private Collection<CacheOperation> parseCacheAnnotations( DefaultCacheConfig cachingConfig, AnnotatedElement ae, boolean localOnly) { Collection<CacheOperation> ops = null; //Find @ cacheable annotation method 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)); } } //Find the method of @ cacheEvict annotation 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)); } } //Find the method of @ cachePut annotation 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)); } } //Find the method of @ Caching annotation 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; }
We can see that this class parses the parameters of @ cacheable, @ cacheEvict, @ cachePut and @ Caching annotations and encapsulates them into the CacheOperation collection.
In addition, the key to the spring cache function is the above Interceptor: CacheInterceptor, which will eventually be called to this method:
@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); } } // Execute the logic of @ CacheEvict. Here, clear the cache when beforeInvocation is true processCacheEvicts(contexts.get(CacheEvictOperation.class), true, CacheOperationExpressionEvaluator.NO_RESULT); // Gets the hit cache object Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class)); //If not, a put request is generated 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 { // If the cache object is not obtained, the business method is called to obtain the return object, which is the key code returnValue = invokeOperation(invoker); cacheValue = unwrapReturnValue(returnValue); } // Collect @ CachePuts data collectPutRequests(contexts.get(CachePutOperation.class), cacheValue, cachePutRequests); // Execute cachePut or a missed Cacheable request and put the returned object into the cache for (CachePutRequest cachePutRequest : cachePutRequests) { cachePutRequest.apply(cacheValue); } // Execute the logic of @ CacheEvict. Here is to clear the cache when beforeInvocation is false processCacheEvicts(contexts.get(CacheEvictOperation.class), false, cacheValue); return returnValue; }
OK, some friends will have a question when they see here:
Since the spring cache has been added, deleted, modified and checked, why do you need @ Caching annotation?
In fact, it is like this: spring considers that in addition to adding, deleting, modifying and querying, if users need to customize their own annotations, or some complex functions need to add, delete, modify and query, they can use the @ Caching annotation.
Another question:
The cache key used in the above example is #type, but if some cache keys are complex and consist of several fields in the entity, how to define this situation?
Let's take a look at the following example:
@Data public class QueryCategoryModel { /** * System number */ private Long id; /** * Parent category number */ private Long parentId; /** * Classification name */ private String name; /** * Classification level */ private Integer level; /** * type */ private Integer type; } @Cacheable(value = "category", key = "#type") public CategoryModel getCategory2(QueryCategoryModel queryCategoryModel) { return getCategoryByType(queryCategoryModel.getType()); }
In this example, all fields of the QueryCategoryModel entity object need to be used as a key. How to define this situation?
1. Customize a class to implement the KeyGenerator interface
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. Define the bean instance of CategoryGenerator in the CacheConfig class
@Bean public CategoryGenerator categoryGenerator() { return new CategoryGenerator(); }
3. Modify the previously defined key
@Cacheable(value = "category", key = "categoryGenerator") public CategoryModel getCategory2(QueryCategoryModel queryCategoryModel) { return getCategoryByType(queryCategoryModel.getType()); }