Java uses mutex to solve cache breakdown problem

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;