[Redis] Redis cache penetration, cache breakdown and cache avalanche solutions

Posted by ultrus on Tue, 14 Dec 2021 05:47:11 +0100

1, Redis cache penetration, cache breakdown and cache avalanche solutions

The conventional use of Redis as a solution will not be described here, but directly cut into the topic. How did Redis's cache penetration, cache breakdown and cache avalanche come from? How?

If you don't have a preliminary understanding and use of Redis, you can read this article to learn the basics and continue to learn more about the problems caused by concurrency, https://blog.csdn.net/qq_38762237/article/details/121608026

The purpose of recording this technical article here is to remind you of the importance of this knowledge point and one of the necessary skills for interview, because one of my short answer questions is to briefly describe what Redis cache penetration, cache breakdown and cache avalanche are, and write solutions

1. Cache penetration

(1) What is cache penetration?

The data corresponding to the key does not exist in the data source. Every time a request for this key cannot be obtained from the cache, the request will be sent to the data source, which may crush the data source. For example, a nonexistent user id is used to obtain user information, and neither the cache nor the database is available. If hackers exploit this vulnerability, they may crush the database

Generally speaking, it refers to data that does not exist in the cache or database

(2) Cache penetration solution

There must be no cache and data that cannot be queried. Because the cache is written passively when it misses, and for the sake of fault tolerance, if the data cannot be found from the storage layer, it will not be written to the cache, which will cause the nonexistent data to be queried in the storage layer every request, losing the significance of the cache

Method 1: bloom filter: hash all possible data into a large enough bitmap, and a non-existent data will be intercepted by the bitmap, so as to avoid the query pressure on the underlying storage system

Method 2: rough method: query and return. Regardless of whether the returned data is empty (data does not exist and system failure), the null value is still cached. Its expiration time will be very short, usually no more than 5 minutes

	/**
	 * Cache penetration solution
	 *
	 * @param cacheKey
	 * @return
	 */
	public String testRedis(String cacheKey) {
		RedisUtil redisUtil = SpringUtils.getBean(RedisUtil.class);
		int cacheTime = 30;
		String cacheValue = redisUtil.get(cacheKey);
		if (StringUtils.isNotEmpty(cacheValue)) {
			System.out.println("Got the cache value:" + cacheValue);
			return cacheValue;
		} else {
			// This means that it is queried from the database, but it is still empty
			cacheValue = "";
			if (StringUtils.isEmpty(cacheValue)) {
				cacheValue = "";
			}
			redisUtil.put(cacheKey, cacheValue, cacheTime);
			System.out.println("Got the cache value:" + cacheValue);
			return cacheValue;
		}
	}

2. Buffer breakdown

(1) What is cache breakdown?

The data corresponding to the key exists, but it expires in redis. At this time, if a large number of concurrent requests come, if these requests find that the cache expires, they will generally load data from the back-end dB and set it back to the cache. At this time, large concurrent requests may crush the back-end DB instantly

Generally speaking, it refers to the data not in the cache but in the database

(2) Cache breakdown solution

Use mutex key

A common practice in the industry is to use mutex. In short, When the cache fails (it is judged that the value is empty), instead of immediately loading dB, first use some operations of the cache tool with the return value of successful operations (such as SETNX of Redis or ADD of Memcache) set a mutex key. When the operation returns success, perform the operation of load db and reset the cache; otherwise, retry the whole get cache method

SETNX is the abbreviation of "SET if Not eXists", that is, it can be set only when it does not exist. It can be used to achieve the effect of locking

	/**
	 * Cache breakdown solution
	 *
	 * @param cacheKey
	 * @return
	 */
	public String testRedis2(String cacheKey) {
		RedisUtil redisUtil = SpringUtils.getBean(RedisUtil.class);
		int cacheTime = 30;
		String keyMutex = "key_mutex";
		String cacheValue = redisUtil.get(cacheKey);
		// Represents that the cache value has expired
		if (StringUtils.isEmpty(cacheValue)) {
			// Set a 3min timeout to prevent the next cache expiration from failing to load db when the del operation fails
			if (redisUtil.setIfAbsent(keyMutex, 1, 3, TimeUnit.MINUTES)) {
				cacheValue = "Obtained from database";
				redisUtil.put(cacheKey, cacheValue, cacheTime);
				redisUtil.delete(keyMutex);
				return cacheValue;
			} else {
				try {
					// At this time, it means that other threads at the same time have loaded dB and set it back to the cache. At this time, you can retry to obtain the cache value
					// retry 
					Thread.sleep(100L);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				return testRedis2(cacheKey);
			}
		} else {
			return cacheValue;
		}
	}

3. Cache avalanche

(1) What is cache avalanche?

When the cache server restarts or a large number of caches fail in a certain period of time, it will also bring great pressure to the back-end system (such as DB)

Generally speaking, it refers to a large area of cache failure at the same time

(2) Cache avalanche solution

The avalanche effect of cache failure has a terrible impact on the underlying system! Most system designers consider locking or queuing to ensure that a large number of threads will not read and write to the database at one time, so as to avoid a large number of concurrent requests falling on the underlying storage system in case of failure. There is also a simple scheme to spread the cache expiration time. For example, we can add a random value based on the original expiration time, such as 1-5 minutes random, so that the repetition rate of each cache expiration time will be reduced, and it is difficult to cause collective failure events

	/**
	 * Cache avalanche solution - lock
	 *
	 * @param cacheKey
	 * @return
	 */
	public String testRedis3(String cacheKey) {
		RedisUtil redisUtil = SpringUtils.getBean(RedisUtil.class);
		int cacheTime = 30;
		String cacheValue = redisUtil.get(cacheKey);
		if (StringUtils.isNotEmpty(cacheValue)) {
			System.out.println("Got the cache value:" + cacheValue);
			return cacheValue;
		} else {
			synchronized (cacheKey) {
				cacheValue = redisUtil.get(cacheKey);
				if (StringUtils.isNotEmpty(cacheValue)) {
					return cacheValue;
				} else {
					// sql query data
					cacheValue = "Get from simulation database";
					redisUtil.put(cacheKey, cacheValue, cacheTime);
				}
			}
			return cacheValue;
		}
	}

Locking queuing is only to reduce the pressure on the database and does not improve the system throughput. Assuming that the key is locked during cache reconstruction under high parallel transmission, 999 of the last 1000 requests are blocked. It will also cause users to wait for timeout, which is a palliative method!

Note: Lock queuing is used to solve the concurrency problem of distributed environment, and it may also solve the problem of distributed lock; Threads will also be blocked, and the user experience is very poor! Therefore, it is rarely used in real high concurrency scenarios!

	/**
	 * Cache avalanche solution - tagged
	 *
	 * @param cacheKey
	 * @return
	 */
	public String testRedis4(String cacheKey) {
		RedisUtil redisUtil = SpringUtils.getBean(RedisUtil.class);
		int cacheTime = 30;
		AtomicReference<String> cacheValue = new AtomicReference<>(redisUtil.get(cacheKey));
		//Cache tag
		String cacheSign = cacheKey + "_sign";
		String sign = redisUtil.get(cacheSign);
		if (StringUtils.isNotEmpty(sign)) {
			// Not expired, return directly
			return cacheValue.get();
		} else {
			redisUtil.put(cacheSign, "1", cacheTime);
			// Start a new thread to execute, or use ThreadPool QueueUserWorkItem
			new Thread(() -> {
				cacheValue.set("Get from simulation database");
				// The date is set to twice the cache time for dirty reads
				redisUtil.put(cacheKey, cacheValue, cacheTime * 2);
			}).start();
			return cacheValue.get();
		}
	}

Explanation:

  • Cache flag: Records whether the cache data has expired. If it has expired, it will trigger another thread to update the cache of the actual key in the background;
  • Cache data: its expiration time is twice as long as that of the cache mark. For example, the mark cache time is 30 minutes and the data cache is set to 60 minutes. In this way, when the cache mark key expires, the actual cache can return the old data to the caller, and will not return the new cache until another thread completes the background update

There are three solutions to cache crash: using locks or queues, setting expiration flags to update the cache, setting different cache expiration times for key s, and a solution called "L2 cache"

Technology sharing area

Topics: Java Redis Cache