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, neither redis nor the database exists. We directly cache the null value corresponding to the id to redis, so that the next time the user repeatedly requests this id, redis can hit (hit null), just 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 inconsistency (controlling the TTL time can be alleviated to a certain extent): When the null is cached, 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 (It can be solved by automatically overwriting the previous null data when inserting data)
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?

The bloom filter can be simply understood as a byte array, which stores binary bits. When it is necessary to determine whether the data in the database exists, it does not directly store the data in the bloom filter, but calculates the hash value through the hash algorithm, and then Convert these hashes to binary bits and store them in the Bloom filter. When judging whether the data exists, the corresponding position can be judged to be 0/1 (this kind of existence is a probabilistic statistic, not 100% accurate, so it does not exist, it does not necessarily exist, so still There is a risk of penetration)
Advantages: less memory usage, no redundant keys (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 if it exists
if (StrUtil.isNotBlank(shopJson)) {
// 3. Exist, return directly
return JSONUtil.toBean(shopJson, Shop.class);
}
// Determine if 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, return error
if (shop == null) {
// write null value 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. return
return shop;
}
Cache Avalanche
Cache avalanche means that a large number of cache keys 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 the TTL of different keys (to solve the problem of simultaneous invalidation): For example, when doing cache warm-up, the data in the database needs to be imported into the cache in batches in advance. Since the data is imported at the same time, the TTL value of these data is the same. This may cause the data to expire at the same time at some point, resulting in an avalanche. In order to solve this problem, we can add a random number to the TTL when importing (for example, TTL is 30±1~5), so that the expiration time of these keys will be scattered in a period of time instead of invalid at the same time, so as to avoid avalanche occur
Use Redis cluster to improve service availability (solve Redis downtime): With the help of the Redis sentinel mechanism, when a machine goes down, the sentinel can automatically select a machine to replace the down machine, and the master and slave can synchronize data to ensure high Redis performance. available
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 multi-level caches to the business: browsers can add caches (usually static resources), reverse-generation server Nginx can add caches, Nginx cache misses and then request Redis, Redis cache misses reach the JVM, and a local cache can also be established inside the JVM , finally reaching the database
cache breakdown
The cache breakdown problem, also known as the hot key problem, is a key that is accessed by 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 time period, 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


/**
* Get the 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 if it exists
if (StrUtil.isNotBlank(shopJson)) {
// 3. Exist, return directly
return JSONUtil.toBean(shopJson, Shop.class);
}
// Determine if the hit is a null value
if (shopJson != null) {
// return an error message
return null;
}

// 4. Implement cache rebuild
// 4.1 Acquire the mutex
String lockKey = LOCK_SHOP_KEY + id;
Shop shop = null;
try {
boolean isLock = tryLock(lockKey);
// 4.2 Determine whether the acquisition is successful
if (!isLock) {
// 4.3 Fail, sleep and try again
Thread.sleep(50);
// recurse
return queryWithMutex(id);
}
// 4.4 Success, query the database according to id
shop = getById(id);
// simulate rebuild delay
Thread.sleep(200);
// 5. does not exist, return error
if (shop == null) {
// write null value 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. return
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, we do not set TTL, and add an expiration time field (not TTL, based on current time + expiration time, logically maintained time) when storing data, so that any thread can query it. Hit, only need to logically determine whether 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. Encapsulate 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 if it exists
if (StrUtil.isBlank(shopJson)) {
// 3. Missed, return directly
return null;
}
// 4. Hit, you need to deserialize json to 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 the store information directly
return shop;
}
// 5.2 has expired and needs to be rebuilt
// 6. Cache rebuild
// 6.1 Acquire the mutex
String lockKey = LOCK_SHOP_KEY + id;
boolean isLock = tryLock(lockKey);
// 6.2 Determine whether the lock is acquired successfully
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 the lock
unlock(lockKey);
}
});
}
// 6.4 Return expired store information
return shop;
}

Related Articles

Explore More Special Offers

  1. Short Message Service(SMS) & Mail Service

    50,000 email package starts as low as USD 1.99, 120 short messages start at only USD 1.00

phone Contact Us