preface

We talked about some memory cache related knowledge points:

  • Understand caching techniques

  • Common memory caching framework

  • Source code analysis for caffeine framework implementation principle

  • Common problems in the use of cache and how to deal with them

So how do you use caching and how do you implement caching

But have you ever thought about it, if you’ve got a system that’s already online, you haven’t used caching before

But now because of the system performance problems need to use caching technology to optimize the existing functions

How can the design be designed to minimize development costs without affecting existing features

Let’s start by reviewing common operations that use caching

  • When querying data, the cache is read first, and if there is no data in the cache, the database is queried and the results are written to the cache. If there is data in the cache, it returns the data in the cache
  • When new data is added, the data is written to the cache
  • When deleting data, delete the corresponding cached data

Let’s say we add new operation cache logic to each interface in the existing system

It certainly increases the workload of the developer and reduces the readability of the program, and also violates the single responsibility of the design principle

Consider implementing a low-intrusion caching framework with Spring AOP and annotations

There is already a framework in Spring that implements this functionality (Spring Cache)

The Spring Cache framework provides a series of annotations. Here are some common ones

  • @cacheable is a method configuration that can be cached based on its request parameters
  • @cacheevict Clears the cache
  • @cachePut Ensures that methods are called and results are expected to be cached. It differs from @cacheable in whether a method is called every time, usually for updates

An example is provided below for a simple use of these annotations

/** * select * from cate_key (value+::key) */
@Cacheable(value = "LoginAccount::account", key = "#account")
@Override
public LoginAccount getByAccount(String account) {
    return super.lambdaQuery()
        .eq(LoginAccount::getAccount, account)
        .one();
}

/** * If the new data is successfully added, the data will be cached */
@CachePut(value = "LoginAccount::account", key = "#loginAccount.account")
public LoginAccount add(LoginAccount loginAccount) {
    super.save(loginAccount);
    return loginAccount;
}

/** * After the data is successfully modified, the data will be cached */
@CachePut(value = "LoginAccount::account", key = "#loginAccount.account")
public LoginAccount update(LoginAccount loginAccount) {
    super.updateById(loginAccount);
    return loginAccount;
}

/** * If the database is deleted successfully, delete the account data from the cache */
@CacheEvict(value = "LoginAccount::account", key = "#account", condition = "#result == true")
public boolean deleteByAccount(String account) {
    return super.lambdaUpdate()
        .eq(LoginAccount::getAccount, account)
        .remove();
}
Copy the code

Custom cache framework

For the use of these annotations, I believe that we will be able to get started soon after reading this simple example or referring to relevant information

But the focus of this tutorial is on how to encapsulate a caching framework like Spring Cache with Spring AOP and custom annotations

Custom annotations

Using @cacheable, @cacheput, @cacheevict as a reference, we’ll start by customizing a few of these annotations

@HTCCacheable

/** * <p> * Custom cache reads annotations * </p> **@authorHeaven hammer science *@date2020/10/24 was the 0024 * /
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface HTCCacheable {

    /** * Cache name */
    String cacheName(a) default "";

    /** * Cache key name */
    String cacheKey(a);

    /** * Cache validity time (unit: second) Default 1 hour */
    int expire(a) default 3600;

    /** * Active cache refresh time (unit: second). Default value -1 indicates that no active cache refresh is performed */
    int refresh(a) default- 1;
}
Copy the code

@HTCCacheEvict

/** * <p> * Custom cache cleanup annotations * </p> **@authorHeaven hammer science *@dateBank against 2020/10/24 0024 * /
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface HTCCacheEvict {

    /** * Cache name */
    String cacheName(a) default "";

    /** * Cache key name */
    String cacheKey(a);

    /** * Specifies whether to clear all cacheName data. */ is not required by default
    boolean allEntries(a) default false;
}
Copy the code

@HTCCachePut

/** * <p> * Custom cache cleanup annotations * </p> **@authorHeaven hammer science *@dateBank against 2020/10/24 0024 * /
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented 
public @interface HTCCacheEvict {

    /** * Cache name */
    String cacheName(a) default "";

    /** * Cache key name */
    String cacheKey(a);

    /** * Specifies whether to clear all cacheName data. */ is not required by default
    boolean allEntries(a) default false;
}
Copy the code

Configure the cache implementation

After customizing the cache annotations, we need to select an in-memory caching framework as the concrete caching implementation

There are several memory caches described earlier

  • The JDK Map
  • ehcache
  • guava
  • caffeine

As mentioned earlier, Caffeine is the best memory caching framework available, so this article will use Caffeine as a specific cache implementation

If you choose Caffeine, you need to configure the implementation of Caffeine. The previous article introduced the loading mode of caffeine

  • Manual loading
  • Synchronous loading
  • Asynchronous loading

This is where manual loading is used to create a Caffeine implementation and the Spring configuration class is used to configure the Caffeine cache

/** * <p> * Custom cache configuration, Spring configuration caffeine * </p> **@authorHeaven hammer science *@date2020/10/24 19:49 * / 0024
@Slf4j
@Configuration
public class HTCCacheConfig {

    /** * config creates a caffeine implementation */ using manual loading
    @Bean("caffeineCache")
    public Cache cache(a) {
        return Caffeine.newBuilder()
                // The maximum number of records cached, exceeding which will be expelled
                .maximumSize(500)
                // Based on time invalidation, start timing invalidation after write
                .expireAfterWrite(1, TimeUnit.HOURS)
                // Cache record statistics
                .recordStats()
                // Cache removal listener
                .removalListener((key, value, cause) -> log.debug(Removed :{} cause:{}", key, cause))
                // Use manual loading mode.build(); }}Copy the code

Custom caching AOP aspect implementation

A utility class that generates cache keys and supports Spring EL expressions will be prepared

The AOP aspect will also be enhanced for methods that use our custom cache annotations

KeyGenerator

/** * <p> * key generation strategy: cache name + cache key (support spring EL expression) * </p> **@authorHeaven hammer science *@date2020/10/24 regained 0024 * /
@Component
public class KeyGenerator {


    /** * for parsing springEL expressions */
    private SpelExpressionParser spelExpressionParser = new SpelExpressionParser();

    /** * The name used to get the method parameter definition */
    private DefaultParameterNameDiscoverer defaultParameterNameDiscoverer = new DefaultParameterNameDiscoverer();


    public String generatorKey(ProceedingJoinPoint point, String cacheName, String cacheKey) throws NoSuchMethodException {
        if (StringUtils.isEmpty(cacheKey)) {
            throw new NullPointerException("CacheKey cannot be empty");
        }

        // If the cache name is empty, the default value is set to the package name of the method
        Signature signature = point.getSignature();
        if (StringUtils.isEmpty(cacheName)) {
            cacheName = signature.getDeclaringTypeName() + "." + signature.getName();
        }

        // Get the target method
        Method method = point.getTarget()
                .getClass()
                .getMethod(signature.getName(), ((MethodSignature) signature).getParameterTypes());

        // Set parameters
        String[] parameterNames = defaultParameterNameDiscoverer.getParameterNames(method);
        Object[] args = point.getArgs();
        EvaluationContext evaluationContext = new StandardEvaluationContext();
        for (int i = 0; i < parameterNames.length; i++) {
            evaluationContext.setVariable(parameterNames[i], args[i]);
        }

        / / parsing cacheKey
        String parseCacheKey = spelExpressionParser.parseExpression(cacheKey).getValue(evaluationContext, String.class);
        return cacheName + "... ""+ parseCacheKey; }}Copy the code

Caching the AOP aspect of annotation collocation

/** * <p> * custom caching AOP facets implementation * </p> **@authorHeaven hammer science *@date2020/10/24 20:06 * / 0024
@Slf4j
@Aspect
@Component
public class HTCCacheAspect {

    @Resource
    private Cache caffeineCache;

    @Resource
    private KeyGenerator keyGenerator;

    /** * define surround notification, read cache data * 1. Parse cache annotation parameter * 2. 3. If there is no query in the cache, execute the target method and store the returned result in the cache */
    @Around(value = "@annotation(htcCacheable)")
    public Object cached(ProceedingJoinPoint point, HTCCacheable htcCacheable) throws Throwable {
        String key = keyGenerator.generatorKey(point, htcCacheable.cacheName(), htcCacheable.cacheKey());
        Object cacheVo = caffeineCache.getIfPresent(key);
        if(cacheVo ! =null) {
            return SerializationUtils.deserialize((byte[]) cacheVo);
        } else {
            try {
                Object vo = point.proceed();
                caffeineCache.put(key, SerializationUtils.serialize(vo));
                return vo;
            } catch (Throwable throwable) {
                log.error("Cache read operation exception :{}", throwable);
                throw new RuntimeException("Cache read operation abnormal", throwable); }}}/** * defines a wrap notification that sets the return value to cache * 1. Executes the target method * 2. Set the result set to cache */
    @Around(value = "@annotation(htcCachePut)")
    public Object cached(ProceedingJoinPoint point, HTCCachePut htcCachePut) {
        try {
            Object vo = point.proceed();
            String key = keyGenerator.generatorKey(point, htcCachePut.cacheName(), htcCachePut.cacheKey());
            caffeineCache.put(key, SerializationUtils.serialize(vo));
            return vo;
        } catch (Throwable throwable) {
            log.error("Cache write operation exception :{}", throwable);
            throw new RuntimeException("Cache write operation exception", throwable); }}/** * define surround notification to delete corresponding cached data * 1. Execute target method * 2. Delete the corresponding cached data */
    @Around(value = "@annotation(hTCCacheEvict)")
    public Object cached(ProceedingJoinPoint point, HTCCacheEvict hTCCacheEvict) {
        try {
            Object vo = point.proceed();
            String key = keyGenerator.generatorKey(point, hTCCacheEvict.cacheName(), hTCCacheEvict.cacheKey());
            caffeineCache.invalidate(key);
            return vo;
        } catch (Throwable throwable) {
            log.error("Cache delete operation exception :{}", throwable);
            throw new RuntimeException("Cache delete operation abnormal", throwable); }}}Copy the code

Use custom cache caching

When we have everything in place, we can replace the cache annotations using Spring Cache with our custom cache framework to see the effect. The sample is as follows

/** * select * from cate_key (value+::key) */
@HTCCacheable(value = "LoginAccount::account", key = "#account")
@Override
public LoginAccount getByAccount(String account) {
    return super.lambdaQuery()
        .eq(LoginAccount::getAccount, account)
        .one();
}

/** * If the new data is successfully added, the data will be cached */
@HTCCachePut(value = "LoginAccount::account", key = "#loginAccount.account")
public LoginAccount add(LoginAccount loginAccount) {
    super.save(loginAccount);
    return loginAccount;
}

/** * After the data is successfully modified, the data will be cached */
@HTCCachePut(value = "LoginAccount::account", key = "#loginAccount.account")
public LoginAccount update(LoginAccount loginAccount) {
    super.updateById(loginAccount);
    return loginAccount;
}

/** * If the database is deleted successfully, delete the account data from the cache */
@HTCCacheEvict(value = "LoginAccount::account", key = "#account", condition = "#result == true")
public boolean deleteByAccount(String account) {
    return super.lambdaUpdate()
        .eq(LoginAccount::getAccount, account)
        .remove();
}
Copy the code

conclusion

The custom cache framework is not intended to subvert or replace the Spring Cache framework

Instead, I want to learn more about how the Spring Cache framework is implemented. What should I think about when I encounter problems

This is the end of the introduction of memory caching technology, thank you for watching