Principles and solutions to Redis cache penetration, breakdown, and avalanche problems

Directory

    • 1. Redis cache penetration
      • 1.1. Principle of cache penetration
      • 1.2. Cache penetration code demonstration
      • 1.3. Cache penetration solution
        • Solution 1 (the key will be cached even if the data cannot be queried in the database)
        • Solution Two (Use Bloom Filter)
    • 2. Redis cache breakdown (cache failure)
    • 3. Redis cache avalanche
      • 3.1. Cache avalanche principle
      • 3.2. Cache avalanche solution

1. Redis cache penetration

1.1. Principle of cache penetration

Cache penetration refers to querying data that does not exist at all. For example, a product table contains product IDs: P001, P002, and P003. When calling the query product details, the product ID: P004 is passed in. P004 must not exist in the cache. The query database logic will be executed directly over the cache layer, and it does not exist in the product table. If the data cannot be queried, the data will not be cached.

  • There are two basic reasons for cache penetration:
    • There is a problem with your own business code or data
    • Malicious attacks, crawlers, etc. cause a large number of air hits

1.2. Cache penetration code demonstration

In the following example code, when the method of getting product details is called and the product ID is P001-3, the product details data can be obtained, and the product details queried in the database will be inserted into Redis, as follows A query for the same product ID will directly read the data in Redis without reading the database again. If the incoming product ID is not P001-3, such as P004, the product does not exist in the database. , if P004 cannot be queried in the database, it will not be cached in Redis. Then every time P004 is queried, the query database logic will be executed. This is a cache penetration problem. The traffic will still be hit even if it is not cached. database.

@Service
public class ProductDetailsService {<!-- -->
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    //Product details key prefix
    private final String PRODUCT_DETAILS_KEY_PREFIX = "PRODUCT_DETAILS_KEY:";
    /**
     * Get product details V1
     * @param productId product ID
     */
    public Object getProductDetailsV1(String productId) {<!-- -->
        if (StringUtils.isEmpty(productId)) {<!-- -->
            throw new RuntimeException("Product ID cannot be empty");
        }
        // cache key
        String productDetailsKey = PRODUCT_DETAILS_KEY_PREFIX + productId;
        // Determine whether the cache exists, and return directly if it exists
        boolean exist = redisTemplate.hasKey(productDetailsKey);
        if (exist) {<!-- -->
            Object value = redisTemplate.opsForValue().get(productDetailsKey);
            System.out.println("Get cached data value=" + value);
            return value;
        }
        // If the product details are not obtained in the cache, query the database
        Object productDetails = getDBProductDetails(productId);
        // If the product details are obtained by querying the database, insert the product details into the cache.
        if (productDetails != null) {<!-- -->
            redisTemplate.opsForValue().set(productDetailsKey, productDetails);
        }
        return productDetails;
    }

    /** Simulate query database */
    private Object getDBProductDetails(String productId) {<!-- -->
        switch (productId) {<!-- -->
            case "P001":
                return "Product P001-Java Programming Thoughts";
            case "P002":
                return "Product P002-The Art of Concurrent Programming in Java";
            case "P003":
                return "Product P003-DDD Domain Driven Design";
        }
        return null;
    }
}

1.3. Cache penetration solution

Solution 1 (If the data cannot be queried in the database, the key will be cached)

Based on the V1 method above, the null value returned by the database that cannot be queried is also cached, but a short expiration time must be set so that the second query will be cached and intercept the traffic. It will not hit the database, which avoids the cache penetration problem in some scenarios, but it cannot avoid malicious attacks. During a malicious attack, the requested product details ID can be different every time. If these product details IDs are stored in the cache, Even if the expiration time is set, Redis will be under great pressure. If the problem of malicious attacks is not considered, this solution can basically solve the cache penetration problem.

 public Object getProductDetailsV2(String productId) {<!-- -->
        if (StringUtils.isEmpty(productId)) {<!-- -->
            throw new RuntimeException("Product ID cannot be empty");
        }
        // cache key
        String productDetailsKey = PRODUCT_DETAILS_KEY_PREFIX + productId;
        // Determine whether the cache exists, and return directly if it exists
        boolean exist = redisTemplate.hasKey(productDetailsKey);
        if (exist) {<!-- -->
            Object value = redisTemplate.opsForValue().get(productDetailsKey);
            System.out.println("Get cached data value=" + value);
            return value;
        }
        // If the product details are not obtained in the cache, query the database
        Object productDetails = getDBProductDetails(productId);

        // If the product details are obtained by querying the database, insert the product details into the cache.
        if (productDetails != null) {<!-- -->
            redisTemplate.opsForValue().set(productDetailsKey, productDetails);
        }else {<!-- -->
            // If the database cannot query the data, it will also be cached and set an expiration time.
            redisTemplate.opsForValue().set(productDetailsKey, null,1, TimeUnit.MINUTES);
        }
        return productDetails;
    }
Solution 2 (using Bloom filter)

For malicious attacks and cache penetration caused by requesting a large amount of non-existent data from the server, you can also use a Bloom filter to filter it first. Bloom filters can generally filter out non-existent data and prevent further requests. Send from backend. When a Bloom filter says a value exists, it probably doesn’t exist; when it says it doesn’t exist, it definitely doesn’t exist.
A Bloom filter is just a large bit array and several different unbiased hash functions. The so-called unbiased means that the hash value of the element can be calculated relatively evenly. When adding a key to a Bloom filter, multiple hash functions will be used to hash the key to calculate an integer index value and then perform a modulo operation on the length of the bit array to obtain a position. Each hash function will calculate a different position. Then set these positions of the bit array to 1 to complete the add operation. When asking the Bloom filter whether the key exists, just like add, it will also calculate several positions of the hash to see if these positions in the bit array are all 1. As long as one bit is 0, then it means This key does not exist in the Bloom filter. If they are all 1, this does not mean that the key must exist, but it is very likely to exist, because these bits are set to 1 because other keys exist. If the bit array is sparse, the probability will be high. If the bit array is crowded, the probability will be reduced. This method is suitable for application scenarios with low data hits, relatively fixed data, and low real-time performance (usually large data sets). The code maintenance is more complex, but the cache space is very small.

Redisson provides an implementation of Bloom filter that can be used directly.

 @Autowired
    private RedissonClient redissonClient;
    public Object getProductDetailsV3(String productId) {<!-- -->
        if (StringUtils.isEmpty(productId)) {<!-- -->
            throw new RuntimeException("Product ID cannot be empty");
        }
        // Determine whether our product details ID exists in the Bloom filter. If not, it means that the system does not have this product details information.
        RBloomFilter<String> bloomFilter = redissonClient.getBloomFilter(PRODUCT_DETAILS_BLOOM_FILTER_KEY_PREFIX);
        if (!bloomFilter.contains(productId)) {<!-- -->
            System.out.println("Determine that the product details do not exist through Bloom filter productId=" + productId);
            return null;
        }
        // cache key
        String productDetailsKey = PRODUCT_DETAILS_KEY_PREFIX + productId;
        // Determine whether the cache exists, and return directly if it exists
        boolean exist = redisTemplate.hasKey(productDetailsKey);
        if (exist) {<!-- -->
            Object value = redisTemplate.opsForValue().get(productDetailsKey);
            System.out.println("Get cached data value=" + value);
            return value;
        }
        // If the product details are not obtained in the cache, query the database
        Object productDetails = getDBProductDetails(productId);
        // If the product details are obtained by querying the database, insert the product details into the cache.
        if (productDetails != null) {<!-- -->
            redisTemplate.opsForValue().set(productDetailsKey, productDetails);
        }
        return productDetails;
    }

    /**
     * Initialize bloom filter
     * When using a Bloom filter, you need to put all data into the Bloom filter in advance, and when adding data, you must also put it into the Bloom filter.
     * Bloom filters can only add data but cannot delete data. If you want to delete it, you must reinitialize the data.
     */
    private void initProductDetailsBloomFilter() {<!-- -->
        RBloomFilter<String> bloomFilter = redissonClient.getBloomFilter(PRODUCT_DETAILS_BLOOM_FILTER_KEY_PREFIX);
        //Initialize the Bloom filter: the expected element is 100000000L, the error rate is 3%, the underlying bit array size will be calculated based on these two parameters
        bloomFilter.tryInit(100000000L, 0.03);
        //Insert the product details ID P001-3 into the Bloom filter
        bloomFilter.add("P001");
        bloomFilter.add("P002");
        bloomFilter.add("P003");
    }

2. Redis cache breakdown (cache failure)

Since the failure of a large batch of caches at the same time may cause a large number of requests to penetrate the cache directly to the database at the same time, it may cause the database to be under instant pressure or even hang up. In this case, when we increase the cache in batches, it is best to cache this batch of data. Expiration times are set to different times within a time period.

3. Redis cache avalanche

3.1. Cache avalanche principle

Cache avalanche means that after the cache layer cannot support or fails, all traffic will be sent to the back-end storage layer. Since the cache layer carries a large number of requests, it effectively protects the storage layer. However, if the cache layer cannot provide services for some reasons (such as extremely large concurrency, the cache layer cannot support it, or due to poor cache design, similar to a large number of requests accessing bigkey). , causing the concurrency that the cache can support to drop sharply), so a large number of requests will hit the storage layer, and the number of calls to the storage layer will increase dramatically, causing cascading downtime of the storage layer.

3.2. Cache avalanche solution

  • 1. Ensure the high availability of cache layer services, such as using Redis Sentinel or Redis Cluster.
  • 2. Conduct high-concurrency testing to ensure that the existing architecture can withstand online concurrency.
  • 3. Use Sentinel or Hystrix service protection components to perform current limiting, fusing and downgrading of business interfaces.