Design and implementation of cache

Reference article

Differences and solutions between cache penetration, cache breakdown, and cache avalanche

Design ideas

When there is no cache, the data is obtained by directly querying the database. If there are too many concurrent queries in a certain period of time, it will cause a lot of pressure on the database, or even crash. At this time we will think of using cache. When querying data, first query the cache, if the cache has it, return it directly, if not, query the database, put the result in the cache and return. It can be obtained directly from the cache in the next query.

There is a commodity table, goods, which optimizes the redis cache for the query of goods. The key and value of goods are as follows.

key = 固定值+version(可变)+参数  = "cache:goods:" + +version(可变)+参数
value = 从数据库查询的结果。

The version is also the value in the redis cache

key = 国定值="cache:goods:version"
value = 1  //初始值1,goods每次增删改,value都加1

Ideas:

  1. Create 2 annotations: CacheVersion, CyCache

    @interface CacheVersion {versionKey,expireTime=3600}:缓存版本
    @interface CyCache {cacheKey,versionKey,expireTime=3600} :缓存
    
  2. Create 2 aspects: CacheVersionAspect, CacheAspect

    class CacheVersionAspect:拦截标记了注解CacheVersion 的增删改方法。
    	在方法执行,CacheVersion.versionKey对应的缓存加1
    class CacheAspect :拦截标记了注解CyCache的查询方法。
    	在执行数据库查询之前先查一下redis缓存(CyCache.cacheKey+version+参数)是否有数据,
    	如果有则直接返回,没有则执行查询方法,得到结果后把结果放入redis中并返回。
    
  3. Add annotation @CacheVersion to the method of adding, deleting and modifying service, and add registration @CyCache to the query method

    通过@CyCache,查询不用每次查询数据库,可以从redis缓存中获取数据。
    通过@CacheVersion,每次增删改后version的值会递增。下次查询时,
    (CyCache.key+version+参数)的缓存就没有(因为version的值变了),需要重新查询数据库并缓存。
    

achieve

Create 2 annotations: CacheVersion, CyCache

package com.example.demo.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 缓存版本注解
 */
@Target({
    
    ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface CacheVersion {
    
    
    /**
     * 版本key
     */
    String[] versionKeys() default {
    
    };

    /**
     * 过期时间,单位秒
     */
    int expireTime() default 3600;
}
package com.example.demo.annotation;


import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 缓存注解
 */
@Target({
    
    ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Cache {
    
    
    /**
     * 版本key 需要和 CacheVersion。versionKey 一样
     */
    String versionKey() default "";

    /**
     * 数据key的固定值部分
     */
    String cacheKey() default "";

    /**
     * 方法的参数集合
     */
    String[] paramName() default {
    
    };

    /**
     * 过期时间,单位秒
     */
    int expireTime() default 3600;
}

Create 2 aspects: CacheVersionAspect, CacheAspect

package com.example.demo.aspect;

import com.example.demo.annotation.CacheVersion;
import com.example.demo.cache.CacheClient;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;

/**
 * 缓存版本处理切面
 */
@Slf4j
@Component
@Aspect
public class CacheVersionAspect {
    
    
    @Resource
    private CacheClient cacheClient;

    @After(value = "@annotation(cacheVersion))")
    public void handlerServiceCacheVersion(JoinPoint point, CacheVersion cacheVersion) {
    
    
        try {
    
    
            String[] versionKeys = cacheVersion.versionKeys();
            for (String versionKey : versionKeys) {
    
    
                cacheClient.increaseCount(versionKey);
                cacheClient.expire(versionKey, cacheVersion.expireTime());
                log.info("update version of : {}", versionKey);
            }
        } catch (Exception e) {
    
    
            log.info("缓存异常:", e);
        }
    }
}


package com.example.demo.aspect;


import com.example.demo.annotation.Cache;
import com.example.demo.cache.CacheClient;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;

@Slf4j
@Component
@Aspect
public class CacheAspect {
    
    
    @Resource
    private CacheClient cacheClient;

    public CacheAspect() {
    
    
    }

    @Around(value = "@annotation(cache))")
    public Object handlerServiceCache(ProceedingJoinPoint point, Cache cache) {
    
    
        try {
    
    
            //获取参数拼凑的key
            String paramKey = getParamKey(point, cache.paramName());
            //获取version的key
            String versionKey = getCacheVersion(cache);
            //dataKey= cache.cacheKey + 参数拼凑的key +version的key
            String dataKey = cache.cacheKey() + paramKey + versionKey;

            // 处理缓存
            Object data = cacheClient.get(dataKey);
            if (data != null) {
    
    
                System.out.println("从缓存中获取数据,key = " + dataKey);
                return data;
            }
            data = point.proceed();
            if (data != null) {
    
    
                cacheClient.set(dataKey, data);
                cacheClient.expire(dataKey, cache.expireTime());
            }
            return data;
        } catch (Throwable e) {
    
    
            log.error("缓存异常:", e);
        }
        return null;
    }

    /**
     * 获取version的key
     */
    private String getCacheVersion(Cache cache) {
    
    
        String versionKey = cache.versionKey();
        if (StringUtils.isBlank(versionKey)) {
    
    
            return "";
        }
        Integer cacheVersion;
        if (cacheClient.get(versionKey) == null) {
    
    
            cacheVersion = cacheClient.increaseCount(versionKey);
        } else {
    
    
            cacheVersion = (Integer) cacheClient.get(versionKey);
        }
        cacheClient.expire(versionKey, cache.expireTime());
        return ":" + cacheVersion.toString();
    }

    /**
     * 获取参数拼凑的key
     */
    private String getParamKey(ProceedingJoinPoint point, String[] params) {
    
    
        if (params == null || params.length == 0) {
    
    
            return "";
        }
        StringBuilder keyExtra = new StringBuilder();
        try {
    
    
            Map<String, Object> nameAndArgs = this.getFieldsName(point, point.getArgs());
            for (String param : params) {
    
    
                if (nameAndArgs.get(param) != null) {
    
    
                    keyExtra.append(":").append(nameAndArgs.get(param));
                }
            }
        } catch (Exception e) {
    
    
            log.info("缓存异常:", e);
        }
        return keyExtra.toString();
    }

    /**
     * 基于Aspect,java1.8以上用java自带的参数反射,以下用spring的实现,似乎跟javassist一样
     */
    private Map<String, Object> getFieldsName(ProceedingJoinPoint point, Object[] args) {
    
    
        Map<String, Object> map = new HashMap<>();
        Signature signature = point.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        String[] names = methodSignature.getParameterNames();
        for (int i = 0; i < args.length; i++) {
    
    
            map.put(names[i], args[i]);
        }
        return map;
    }

}

Add annotation @CacheVersion to the method of adding, deleting and modifying service, and add registration @CyCache to the query method

package com.example.demo.service;

import com.example.demo.annotation.Cache;
import com.example.demo.annotation.CacheVersion;
import com.example.demo.demain.Good;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

@Service
public class GoodService {
    
    
    private Integer goodNum = 2;
    private String description = "好商品";


    @Cache(cacheKey = "cache:good", versionKey = "cache:good:version", paramName = {
    
    "name"})
    public List<Good> getGoodList(String name) {
    
    
        System.out.println("从数据库中获取数据");
        List<Good> goods = new ArrayList<>();
        for (int i = 1; i <= goodNum; i++) {
    
    
            Good good = Good.builder().id(i).name(i + "号商品").price(2000L).description(i + "号商品 " + description).build();
            goods.add(good);
        }
        return goods;
    }

    @CacheVersion(versionKeys = {
    
    "cache:good:version"})
    public void addGood() {
    
    
        System.out.println("增长商品");
        goodNum++;
    }

    @CacheVersion(versionKeys = {
    
    "cache:good:version"})
    public void deleteGood() {
    
    
        System.out.println("减少商品");
        goodNum--;
    }

    @CacheVersion(versionKeys = {
    
    "cache:good:version"})
    public void updateGood(Integer id, String description) {
    
    
        System.out.println("修改商品描述 description = " + description);
        this.description = description;
    }

}

test

package com.example.demo.service;

import com.alibaba.fastjson.JSON;
import com.example.demo.demain.Good;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import javax.annotation.Resource;
import java.util.List;

@SpringBootTest
class GoodServiceTest {
    
    
    @Resource
    private GoodService goodService;

    @Test
    void getGoodList() {
    
    
        System.out.println("第1次查询:");
        List<Good> goods = goodService.getGoodList("你好");
        System.out.println(JSON.toJSONString(goods));
        System.out.println("第2次查询:");
        goods = goodService.getGoodList("你好");
        System.out.println(JSON.toJSONString(goods));

        System.out.println("########## 修改商品 ##########");
        goodService.updateGood(2,"非常好的商品");
        System.out.println("########## 修改商品 ##########");

        System.out.println("修改后第1次查询:");
        goods = goodService.getGoodList("你好");
        System.out.println(JSON.toJSONString(goods));
        System.out.println("修改后第2次查询:");
        goods = goodService.getGoodList("你好");
        System.out.println(JSON.toJSONString(goods));
    }
}

operation result
Insert picture description here

problem

1. Cache avalanche

The cache is invalid and all requests are sent to the database. It is very likely that our database will be destroyed and the entire service will be paralyzed! The possible causes of cache invalidation are as follows:

1. When Redis caches data, an expiration time needs to be set. If a large number of expiration times are the same, in extreme cases, these caches may become invalid at the same time, and all requests are sent to the database.

2. Redis is down, and all requests are sent to the database.
,
How to solve the cache avalanche

The first case

Add a random value to the expiration time when caching, which will greatly reduce the cache expiration at the same time.

The second case can be dealt with in three steps

Before the incident :

  1. Redis persistence.
  2. Realize the high availability of Redis (master-slave architecture + Sentinel or Redis Cluster) to avoid all Redis hangs.
  3. Set the backup cache, such as the local cache (ehcache), when the redis cache avalanche, enable the backup cache.

During the incident :

  1. Use hystrix to limit and downgrade source service access. Downgrading is very normal in a high-concurrency system: for example, in a recommendation service, if the personalized recommendation service is not available, you can downgrade to supplement hotspot data. Can refer to the article: Hystrix learning summary
  2. Enable backup cache

After the incident :

  1. Restart now. After redis is persisted, redis automatically loads data from the disk after restarting, and quickly restores cached data.

2. Cache penetration

Cache penetration refers to querying a data that must not exist. Due to cache misses, and for fault tolerance considerations, if data cannot be found from the database,
it will not be written to the cache. This will cause the non-existent data to be queried in the database every time a request is made, and the meaning of the cache is lost. This is cache penetration: a large number of requested data misses in the cache, causing the request to go to the database. If cache penetration occurs, it may also bring down our database and cause the entire service to be paralyzed!

How to solve the cache avalanche

  1. Since the requested parameters are illegal (request for non-existent parameters every time), we can use BloomFilter or compression filter to intercept in advance. If it is illegal, we won't let this request go to the database layer!
  2. When we can't find it from the database, we also set this empty object into the cache. The next time you request it, you can get it from the cache. In this case, we generally set a shorter expiration time for empty objects.

3. Cache breakdown

Cache breakdown means that there is no data in the cache but the data in the database (usually when the cache time expires). At this time, because there are so many concurrent users, and the data is not read in the read cache at the same time, the data is retrieved from the database at the same time, causing database pressure Increase instantly, causing excessive pressure

How to solve the cache avalanche

  1. Set hotspot data to never expire.
  2. Add the mutex lock, the reference code for the mutex lock is as follows:
    Insert picture description here

Description:

1) There is data in the cache, and the result will be returned after walking the 13 lines of the above code directly

2) There is no data in the cache. The first thread that enters acquires the lock and fetches data from the database. Before releasing the lock, other threads that enter in parallel will wait for 100ms, and then re-cache to fetch data. This prevents repeated fetching of data from the database and repeated updating of data in the cache.

3) Of course, this is simplified processing. In theory, it would be better to lock based on the key value. That is, thread A fetching key1 data from the database does not prevent thread B from fetching key2 data. The above code obviously cannot do this.

Guess you like

Origin blog.csdn.net/fangye1/article/details/111408077