1. Cache breakdown principle
The cache breakdown problem is also called the hotspot key problem, which means that a key with high concurrent access and complex cache business suddenly fails, and countless access requests bring a huge impact to the database in an instant.
2. The principle of mutual exclusion lock
The mutual exclusion lock is used after the query cache misses. In order to prevent countless requests from suddenly entering the database, we set up the relationship between multiple threads as a mutual exclusion relationship. When a thread accesses the database, another The thread must wait. When the thread accessing the database finishes accessing and writes to the cache, another thread is allowed to access.
If the query cache misses, the mutex will be acquired. If the acquisition is successful, it will enter the database query and rebuild the cache data; if the acquisition fails, it will enter a period of dormant waiting time (this time depends on the length of time the project queries the database). The cache will be queried again after hibernation ends.
3. Advantages and disadvantages of mutex
Advantages:
No additional memory consumption. The mutex is closed after the thread accesses the database, so there is no additional memory consumption.
Guaranteed consistency. If thread 1 fails to acquire the mutex, it will re-query the cache after a period of time. After the thread accessing the database ends and writes the data into the cache, when the thread queries the cache again, the obtained data will be consistent with the database. Therefore, the data consistency of the cache and the database is guaranteed.
Easy to implement. It can be realized only by making a judgment, and the realization is very simple.
Disadvantages:
Thread needs to wait. Since it takes time for the thread to query the database, it must wait when other threads fail to access the mutex, thus consuming additional time.
Performance impacted.
There may be a risk of deadlock.
4. Scenario assumption
First assume a scenario: we need to query store information based on the store id.
After submitting the store id, we need to query the store cache from Redis first.
If the cache hits, the data is returned directly;
If the cache does not hit, then you need to try to acquire the mutex, that is, to determine whether there are other threads accessing the database at this time. If the acquisition is successful, it means that no thread is accessing the database, then query the database according to the id, exit the database after querying, and write the store information into Redis. After this the mutex is released and the data is returned. The following is the business logic flowchart.
5. Code implementation
First, customize the two functions of acquiring locks and closing locks according to the business logic. This is implemented using Redis’s SortedSet data type. We zset a key into the cache, and if successful, acquire the mutex. At the same time, an expiration time must be set, so that the key can be deleted in time when the query time of the database is too long or a failure occurs, that is, the lock is closed to allow other threads to access the database.
//Get the lock private boolean tryLock(String key) { Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", LOCK_SHOP_TTL, TimeUnit.SECONDS); //The bottom layer of unboxing is to call the booleanValue() method. If the flag is null, a null pointer exception will occur return BooleanUtil.isTrue(flag); } //Close the lock private void unlock(String key) { stringRedisTemplate.delete(key); }
Then there is the implementation of the business process. The following is the complete implementation code, with a total of eight steps.
@Service public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService { @Resource private StringRedisTemplate stringRedisTemplate; @Override public Result queryById(Long id) { // cache penetration // Shop shop = queryWithPassThrough(id); // Mutex to solve cache breakdown Shop shop = queryWithMutex(id); if (shop == null) { return Result.fail("The store does not exist"); } // return return Result.ok(shop); } //mutex lock public Shop queryWithMutex(Long id) { String key = CACHE_SHOP_KEY + id; // 1. Query store cache from redis String shopJson = stringRedisTemplate.opsForValue().get(key); // 2. To determine whether it exists, that is, whether there is real data, isNotBlank will determine whether the string has a value, and if it is an empty string, it will still return false if (StrUtil. isNotBlank(shopJson)) { // 3, exist, return directly return JSONUtil.toBean(shopJson, Shop.class); } // Determine whether the hit is a null value, !=null means that an empty string is hit, has a value, but has no content (no real data) if(shopJson != null){ // return an error message return null; } // implement cache rebuild // 4.1. Acquire a 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. If it fails, sleep and try again Thread. sleep(50); // Retry query cache from redis - recursive return queryWithMutex(id); } //Note: If the lock is acquired successfully, you should check whether the redis cache exists again and do a DoubleCheck. No need to rebuild cache if present // 4.4, success, query the database according to id shop = getById(id); // Simulate rebuild delay Thread. sleep(200); // 5, does not exist, returns an error if (shop == null) { // TODO cache penetration: the data requested by the user does not exist in the cache or in the database, and continuously initiating such requests will bring huge pressure to the database // TODO Solution 1: Cache null values, Solution 2: Bloom filtering, Solution 3: Enhance the complexity of ids to avoid guessing id rules, Solution 4: Do a good job of basic data format verification, Solution 5: Strengthen user permissions Verification, plan six: Do a good job of current limiting of hotspot parameters //write null value to redis stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES); // return error message 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; } }
Step 1: Query store cache from redis
String key = CACHE_SHOP_KEY + id; // 1. Query store cache from redis String shopJson = stringRedisTemplate.opsForValue().get(key);
Steps 2 and 3: Determine whether the cache exists, and return if it exists. “== null” cannot be used here, because there may be an empty string, and it should be judged whether there is real data. isNotBlank will determine whether the string has a value, and return false if it is an empty string.
// 2. Determine whether it exists, that is, whether there is real data. isNotBlank will determine whether the string has a value. If it is an empty string, it will still return false if (StrUtil. isNotBlank(shopJson)) { // 3, exist, return directly return JSONUtil.toBean(shopJson, Shop.class); }
It should also be judged here whether the following is an empty string. This may happen, but it is unreasonable, so an error message is returned.
// Determine whether the hit is a null value, !=null means hit an empty string, has a value, but no content (no real data) if(shopJson != null){ // return an error message return null; }
Step 4: Acquire the lock, and query the database after success.
// Implement cache rebuilding // 4.1. Acquire a 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. If it fails, sleep and try again Thread. sleep(50); // Retry query cache from redis - recursive return queryWithMutex(id); } //Note: If the lock is acquired successfully, you should check whether the redis cache exists again and do a DoubleCheck. No need to rebuild cache if present // 4.4, success, query the database according to id shop = getById(id); // Simulate rebuild delay Thread. sleep(200);
Step 5: Return an error if it does not exist.
// 5. Does not exist, returns an error if (shop == null) { // TODO cache penetration: the data requested by the user does not exist in the cache or in the database, and continuously initiating such requests will bring huge pressure to the database // TODO Solution 1: Cache null values, Solution 2: Bloom filtering, Solution 3: Enhance the complexity of ids to avoid guessing id rules, Solution 4: Do a good job of basic data format verification, Solution 5: Strengthen user permissions Verification, plan six: Do a good job of current limiting of hotspot parameters //write null value to redis stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES); // return error message return null; }
Step 6: Rebuild the cache.
// 6. Exist, write to redis stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES); } catch (InterruptedException e) { throw new RuntimeException(e); }
Steps 7 and 8: Release (close) the mutex and return.
finally { // 7. Release the mutex unlock(lockKey); } // 8. Return return shop;