Detailed explanation of Redis caching problems

cache penetration

  • Cache penetration means that the data requested by the client does not exist in the cache and the database, so the cache will never take effect, and these requests will hit the database
  • If a malicious user uses countless threads to concurrently access non-existent data, these requests will all reach the database, which is likely to crash the database

solution

Cache empty objects

  • Idea: When a user requests an id, redis and the database do not exist. We cache the null value corresponding to the ID directly to redis so that the next time the user repeatedly requests the id, the redis can hit (hit null) and will not ask the database
  • Advantages: simple to implement, easy to maintain
  • shortcoming:

    • Additional memory consumption (can be solved by adding TTL)

-  may cause short-term inconsistencies (control TTL Time can be alleviated to a certain extent): when the cache is null , we just set the value in the database, and the user query is null,But it actually exists in the database, which will cause inconsistency (automatically overwrite the previous data when inserting data null data can be resolved)

Bloom filter

  • A layer of bloom filter is added between the client and redis. When the user accesses, there is a bloom filter to determine whether the data exists. If it does not exist, it will be rejected directly; if it exists, the normal process can be processed.
  • How does a Bloom filter determine if data exists?

    • Bloom filters can be simply understood as byte arrays, which store binary bits. When you want to determine whether there is data in the database, you do not store the data directly in the Bloom filter. Instead, you calculate the hash value through a hash algorithm, and then Convert these hashes to binary bits and store them in the Bloom filter. When determining the existence of data, it is sufficient to determine whether the corresponding location is 0/1 (this existence is a probability statistic, not 100% accurate, so Does not exist really does not exist, does not necessarily exist, so there is still a risk of penetration)
  • Advantages: less memory usage, no redundant key s (binary)
  • shortcoming:

    • complex to implement
    • There is a possibility of misjudgment (not necessarily accurate)

Cache empty objects Java implementation

/**
 * cache penetration
 *
 * @param id
 * @return
 */
public Shop queryWithPassThrough(Long id) {
    String key = CACHE_SHOP_KEY + id;
    // 1. Query the store cache from redis
    String shopJson = stringRedisTemplate.opsForValue().get(key);
    // 2. Determine whether there is
    if (StrUtil.isNotBlank(shopJson)) {
        // 3. Exist, return directly
        return JSONUtil.toBean(shopJson, Shop.class);
    }
    // Determine whether the hit is a null value
    if (shopJson != null) {
        // return an error message
        return null;
    }
    // 4. Does not exist, query the database according to the id
    Shop shop = getById(id);
    // 5. does not exist, returns an error
    if (shop == null) {
        // Write empty values ​​to redis (cache penetration)
        stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
        return null;
    }
    // 6. Exist, write to redis
    stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
    // 7. Back
    return shop;
}

Cache Avalanche

  • Cache avalanche means that a large number of cache key s are invalid at the same time or the Redis service is down at the same time, resulting in a large number of requests reaching the database, bringing huge pressure

  • solution

    • Add random values to TTLs for different keys (to solve the problem of simultaneous invalidation): For example, when doing cache warm-up, you need to import data from the database in to the cache in batch ahead of time. Since these data have the same TTL value at the same time, This may cause the data to expire at the same time at some point, resulting in an avalanche. To solve this problem, we can add a random number to the TTL when importing (for example, TTL is 30 +1~5), so that these expiration times of the key will be scattered over a period of time, rather than invalid at the same time, thus avoiding avalanche occur rence
    • Use Redis clusters to improve the available ness of services (resolving Redis downtime): With the Redis Sentinel mechanism, when a machine is down, the Sentry can automatically select a machine to replace the downtime machine, while the master and slave can synchronize data, thus ensuring Redis's Haigh availability
    • Add a downgrade and current limiting strategy to the cache business: such as fast failure, denial of service, and preventing requests from being pushed into the database
    • Add a multilevel cache to your business: browsers can add caches (typically static resources), antigen servers Nginx can add caches, Nginx caches miss and request Redis, Redis caches Miss reach JVM, local caches can also be built inside JVM, finally reaching the database

cache breakdown

  • The cache breakdown problem, also known as the hot key problem, is a key that is accessed with high concurrent access and has a complex cache reconstruction business suddenly fails. Countless request accesses will have a huge impact on the database in an instant.
Cache reconstruction: The cache in redis will be invalid after expiration, and it needs to be re-queried from the database and written to redis after the expiration. The process of querying and constructing data from the database may be complicated, requiring multi-table join queries, etc., and finally the results are cached. This business may take a long time (tens or even hundreds of milliseconds). During this period of time, there is no cache in redis, and incoming requests will miss to access the database.

solution

Mutex

  • When a thread request is found to be missed, the lock operation is performed before querying the database, and the lock is released after writing to the cache. In this way, when other threads are missed, the mutex lock will also be acquired when querying the database. After the acquisition fails, it will sleep for a period of time and then query again.
  • Obviously, other threads can obtain data only after writing to the cache. Although consistency can be guaranteed, the performance is relatively poor, and it may cause deadlock.

  • Java implementation

/**
 * acquire lock
 *
 * @param key
 * @return
 */
private boolean tryLock(String key) {
    Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", LOCK_SHOP_TTL, TimeUnit.SECONDS);
    return BooleanUtil.isTrue(flag);
}


/**
 * release lock
 *
 * @param key
 */
private void unlock(String key) {
    stringRedisTemplate.delete(key);
}


/**
 * Mutex
 *
 * @param id
 * @return
 */
public Shop queryWithMutex(Long id) {
    String key = CACHE_SHOP_KEY + id;
    // 1. Query the store cache from redis
    String shopJson = stringRedisTemplate.opsForValue().get(key);
    // 2. Determine whether there is
    if (StrUtil.isNotBlank(shopJson)) {
        // 3. Exist, return directly
        return JSONUtil.toBean(shopJson, Shop.class);
    }
    // Determine whether the hit is a null value
    if (shopJson != null) {
        // return an error message
        return null;
    }

    // 4. Implement cache reconstruction
    // 4.1 Acquiring a mutex
    String lockKey = LOCK_SHOP_KEY + id;
    Shop shop = null;
    try {
        boolean isLock = tryLock(lockKey);
        // 4.2 Judging whether the acquisition is successful
        if (!isLock) {
            // 4.3 Fail, sleep and try again
            Thread.sleep(50);
            // recursion
            return queryWithMutex(id);
        }
        // 4.4 Success, query the database according to id
        shop = getById(id);
        // Analog reconstruction delay
        Thread.sleep(200);
        // 5. does not exist, returns an error
        if (shop == null) {
            // Write empty values ​​to redis (cache penetration)
            stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
            return null;
        }
        // 6. Exist, write to redis
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    } finally {
        // 7. Release the mutex
        unlock(lockKey);
    }
    // 8. Back
    return shop;
}
  • Let's test it with jmeter, send 1000 requests, we can see that all requests are passed, and the database is only queried once


logical expiration

  • As the name suggests, it is not really expired, it can be seen as never expired. When we cache data in redis without setting TTL, add an expiration time field to store data (not TTL, current time + expiration time, logically maintained time), so that any thread can Hit, only need to logically determine if it has expired
  • As shown in the figure below, if thread 1 finds that the logical time has expired when querying the cache, it needs to rebuild the cache and then acquire the mutex. ) instead of performing the cache reconstruction operation by itself. After the cache reconstruction is completed, the lock is released, and thread 1 directly returns the expired data. When other threads are also missed, the failure to acquire the mutex will directly return expired data. Although the performance is guaranteed, the consistency cannot be guaranteed.

  • Java implementation
/**
 * cache warm-up
 *
 * @param id
 * @param expireSeconds logical expiration time
 */
public void saveShop2Redis(Long id, Long expireSeconds) throws InterruptedException {
    // 1. Query store data
    Shop shop = getById(id);
    Thread.sleep(200);
    // 2. Encapsulation logic expiration time
    RedisData redisData = new RedisData();
    redisData.setData(shop);
    redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
    // 3. Write to redis
    stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
}

private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

/**
 * logical expiration
 *
 * @param id
 * @return
 */
public Shop queryWithLogicalExpire(Long id) {
    String key = CACHE_SHOP_KEY + id;
    // 1. Query the store cache from redis
    String shopJson = stringRedisTemplate.opsForValue().get(key);
    // 2. Determine whether there is
    if (StrUtil.isBlank(shopJson)) {
        // 3. Missed, return directly
        return null;
    }
    // 4. Hit, you need to deserialize json into an object first
    RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
    Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
    LocalDateTime expireTime = redisData.getExpireTime();
    // 5. Determine whether it has expired
    if (expireTime.isAfter(LocalDateTime.now())) {
        // 5.1 If it has not expired, return to the store information directly
        return shop;
    }
    // 5.2 Expired, need to rebuild the cache
    // 6. Cache rebuild
    // 6.1 Acquiring a Mutex
    String lockKey = LOCK_SHOP_KEY + id;
    boolean isLock = tryLock(lockKey);
    // 6.2 Determine whether the lock is successfully acquired
    if (isLock) {
        // 6.3 Success, open an independent thread to achieve cache reconstruction
        CACHE_REBUILD_EXECUTOR.submit(() -> {
            try {
                // rebuild cache
                this.saveShop2Redis(id, 20L);
            } catch (Exception e) {
                throw new RuntimeException(e);
            } finally {
                // release lock
                unlock(lockKey);
            }
        });
    }
    // 6.4 Return expired store information
    return shop;
}

Compared

solutionadvantageshortcoming
MutexNo extra memory consumption
Guaranteed Consistency
Simple to implement
Threads need to wait, performance is affected
possible deadlock risk
logical expirationThe thread does not need to wait, and the performance is betterConsistency is not guaranteed
Has extra memory consumption
complex to implement

Tags: Java Database Nginx Redis Algorithm Cache nosql Storage

Posted by bliss322 on Sun, 25 Sep 2022 21:01:59 +0300