Dark Horse Comments-07 Cache breakdown problem (hotspot key failure) and solution, mutex lock and setting logical expiration time

Cache breakdown problem (hotspot key invalidation)

The cache breakdown problem is also called the hot key problem. It means that a key that is accessed by highly concurrently and has a complicated cache rebuilding business suddenly fails. At this time, countless requests and accesses will hit the database in an instant. , brings huge impact

  • The key of a product in a flash sale suddenly becomes invalid. Since everyone is rushing to buy it, there will be countless requests to access the database directly at this moment, causing cache breakdown.

Mutex lock

If the corresponding store information is not cached in the cache, all threads need to obtain the lock before querying the store information in the database. This ensures that only one thread accesses the database and avoids excessive database access pressure

  • Advantages: Simple implementation and no additional memory destruction (adding a lock). When the thread that obtains the thread lock rebuilds the cache data, the data queried from the cache and the data in the database will be consistent when other threads access it again.
  • Disadvantages: When the thread that has obtained the thread lock is operating the database, other threads can only wait, changing the performance of the query from parallel to serial (the tryLock method + double check can solve this), but there is still the risk of deadlock.

setnx implements mutex lock

Query store information based on store ID, adding the step of obtaining a mutex lock, that is, when the cache misses, only the thread that successfully obtains the lock can query the database, ensuring that only one thread goes to the database Execute query statements to prevent cache breakdown

Use the setnx key (lock Id) value command provided by redis to determine whether a thread has successfully inserted the key (lock). del key means releasing the lock

Return value Description
0 Indicates that the thread fails to insert the key, that is, the thread fails to acquire the lock
1 Indicates that the thread inserts the key successfully, that is, the thread acquires the lock successfully

The method corresponding to the setnx instruction in StringRedisTemplate is setIfAbsent(). Returning true means the insertion is successful, and fasle means the insertion failed

//Each store has its own lock. Try to obtain the lock based on the lock's ID (lock prefix + store ID) (essentially inserting the key)
private boolean tryLock(String key) {<!-- -->
    Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
    // We use the BooleanUtil tool class here to convert Boolean type variables into boolean to avoid returning null during the unboxing process
    return BooleanUtil.isTrue(flag);
}

// Release the lock (essentially delete the key)
private void unlock(String key) {<!-- -->
    stringRedisTemplate.delete(key);
}

Separately implement the method queryWithMutex that is responsible for solving the problem of cache breakdown. In this method, if the store information is found and the store cannot be found, null will be returned. Finally, < Make unified judgment and return result class in code>queryById

  • If the lock is acquired successfully, you should check again whether the redis cache exists, because at this time, other threads may have rebuilt the cache and just released the lock. Do a double check. If it exists, there is no need to rebuild the cache.

@Override
public Result queryById(Long id) {<!-- -->
    //Use mutex lock to solve cache breakdown
    Shop shop = queryWithMutex(id);
    // If shop equals null, it means that the corresponding store does not exist in the database or the cached store information is an empty string.
    if (shop == null) {<!-- -->
        return Result.fail("The store does not exist!!");
    }
    // shop is not equal to null, return the queried merchant information to the front end
    return Result.ok(shop);
}
@Override
public Shop queryWithMutex(Long id) {<!-- -->
    //1. First query the corresponding store cache information from Redis. The constant value here is the fixed store prefix + query the store's Id.
    String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
    //2. If the store information is queried in Redis, and the store information is not an empty string, it will be converted to Shop type and returned directly. "", null and "/t/n (line feed)" will be judged to be empty and false will be returned.
    if (StrUtil.isNotBlank(shopJson)) {<!-- -->
        Shop shop = JSONUtil.toBean(shopJson, Shop.class);
        return shop;
    }
    //3. If the hit is an empty string, which is the empty data we cache, return null
    if (shopJson != null) {<!-- -->
        return null;
    }
    // 4. If there is no hit, try to obtain the mutex lock (essentially inserting the key) based on the lock's Id (lock prefix + store Id) to implement cache reconstruction.
    // Calling Thread's sleep method will throw an exception. You can use try/catch/finally to wrap up the process of acquiring and releasing the lock.
    Shop shop = null;
    try {<!-- -->
        // 4.1 Get the mutex lock
        boolean isLock = tryLock(LOCK_SHOP_KEY + id);
        // 4.2 Determine whether the lock acquisition is successful (whether the key insertion is successful)
        if(!isLock){<!-- -->
            //4.3 If the lock acquisition fails (key insertion fails), then sleep for a period of time and re-query the store cache (recursively)
            Thread.sleep(50);
            return queryWithMutex(id);
        }
        //4.4 If the lock is acquired successfully (the key is inserted successfully), the database is queried according to the store's Id.
        shop = getById(id);
        // Since querying the database locally is faster, you can simulate reconstruction delays and trigger concurrency conflicts here.
        Thread.sleep(200);
        // 5. If the corresponding store cannot be found in the database, write the empty string to Redis and set the validity period.
        if(shop == null){<!-- -->
            stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
            return null;
        }
        //6. The store information is found in the database, that is, shop is not null, convert the shop object into a json string, write it to redis and set the TTL
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL,TimeUnit.MINUTES);
    }catch (Exception e){<!-- -->
        throw new RuntimeException(e);
    }
    finally {<!-- -->
        //7. Regardless of whether there will be an exception before, the lock must be released eventually.
        unlock(lockKey);
    }
    //Finally return the queried merchant information to the front end
    return shop;
}

Testing mutex locks to resolve cache breakdown

Use Jmeter to simulate a cache breakdown scenario. At a certain moment, the cached TTL of a hot store expires. At this time, users cannot obtain the cache data of the hot store from Redis, and then they have to Go to the database to query store information

  • First delete the cached data of hot stores in Redis to simulate TTL expiration, and then use Jmete to open 100 threads to access this uncached store information

  • If the background log only outputs one SQL statement, it means that our mutex is in effect and has not caused a large number of users to go to the database to execute SQL statements to query store information.

PLAINTEXT
: ==> Preparing: SELECT id,name,type_id,images,area,address,x,y,avg_price,sold,comments,score,open_hours,create_time,update_time FROM tb_shop WHERE id=?
: ==> Parameters: 2(Long)
: <== Total: 1

Logical expiration (cache warm-up)

The main reason for the cache breakdown problem is that we set the expiration time for the key. Assuming that we do not set the expiration time, there will be no cache breakdown problem. However, if we do not set the expiration time, the cached data will continue to occupy memory< /strong>

  • Advantages: Build cache through asynchronous threads to avoid waiting for other threads and improve performance
  • Disadvantages: Building an asynchronous thread business is complex, and requires maintaining an expire field, which requires additional memory consumption. Before the asynchronous thread completes building the cache, other threads return expired data (dirty data), resulting in data inconsistency.

Logical expiration application

Implement query store business based on store ID, and solve cache breakdown problem based on logical expiration method (hotspot key needs to be added in advance)

Step 1: Because the value of the data stored in redis now needs to have an expiration time attribute, you can create a new entity class containing the original data and expiration time fields (without invading the original code)

@Data
public class RedisData {<!-- -->
    // Expiration
    private LocalDateTime expireTime
    //Original data (use universal Object)
    private Object data;
}

Step 2: Add a new method in ShopServiceImpl, use unit testing to cache preheating i.e. add hotspot key, encapsulate hotspot store information and expiration time fields into RedisData objects and write them Redis cache

public void saveShop2Redis(Long id, Long expirSeconds) {<!-- -->
    // 1. Query store data in the database according to the store ID
    Shop shop = getById(id);
    // Since local query database is faster, simulate reconstruction delay
    Thread.sleep(200);
    // 2. Encapsulate the logical expiration time (convert the current time to seconds)
    RedisData redisData = new RedisData();
    //Set hot store information
    redisData.setData(shop);
    redisData.setExpireTime(LocalDateTime.now().plusSeconds(expirSeconds));
    // 3. Convert the RedisData object containing hot store information and logical expiration time fields into a JSON string and cache it to Redis
    stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
}

Step 3: Run the test method in the test class, and then go to the Redis graphical page to view the stored value (including the data field, that is, the shop object and the expireTime logical expiration time field)

@SpringBootTest
class HmDianPingApplicationTests {<!-- -->
    @Autowired
    private ShopServiceImpl shopService;
    @Test
    public void test(){<!-- -->
        shopService.saveShop2Redis(1L,1000L);
    }
}
{<!-- -->
    "data": {<!-- -->
        "area": "大关",
        "openHours": "10:00-22:00",
        "sold": 4215,
        "images": "https://qcloud.dpfile.com/pc/jiclIsCKmOI2arxKN1Uf0Hx3PucIJH8q0QSz-Z8llzcN56-IdNpm8K8sG4.jpg",
        "address": "No. 29, Jinchang Wenhua Court, Jinhua Road",
        "comments": 3035,
        "avgPrice": 80,
        "updateTime": 1666502007000,
        "score": 37,
        "createTime": 1640167839000,
        "name": "476 Tea Restaurant",
        "x": 120.149192,
        "y": 30.316078,
        "typeId": 1,
        "id": 1
    },
    "expireTime": 1666519036559
}

Step 4: Write the queryWithLogicalExpire method. In this method, if the store information is found, it returns null if the shop cannot be found. Finally, unify it in the queryById method. Determine and return the result class

//Declare a thread pool, because using logical expiration to solve cache breakdown requires a new thread to complete the reconstruction of the cache.
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

@Override
public Result queryById(Long id) {
    //Test using logical expiration to solve cache breakdown
    Shop shop = queryWithLogicalExpire(id);
    // If shop equals null, it means that the corresponding store does not exist in the database or the cached store information is an empty string.
    if (shop == null) {
        return Result.fail("The store does not exist!!");
    }
    // shop is not equal to null, return the queried merchant information to the front end
    return Result.ok(shop);
}

public Shop queryWithLogicalExpire(Long id) {
    //1. First query the corresponding hot store cache information (including expiration time) from Redis. The constant value here is the fixed store prefix + query store Id.
    String json = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
    //2. If it is not hit, that is, json is equal to null, or if it is hit, but json is equal to the empty string, null will be returned directly (indicating that we have not imported the corresponding key)
    //"", null and "/t/n (line feed)" will all be judged as empty and return false
    if (StrUtil.isBlank(json)) {
        return null;
    }
    //3. If the hot store information is queried in Redis and it is not an empty string, convert the JSON string into a RedisData object
    RedisData redisData = JSONUtil.toBean(json, RedisData.class);
    //4. The essential type of redisData.getData() is JSONObject type (or JSON string) and not Object type object, so it cannot be directly converted to Shop type, and a tool class is required.
    Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
    //5. Get the expiration time encapsulated in the RedisData object and determine whether it has expired.
    LocalDateTime expireTime = redisData.getExpireTime();
    if(expireTime.isAfter(LocalDateTime.now())) {
        // 5.1. Not expired, return store information directly
        return shop;
    }
    // 6. It has expired and needs to be cached again. Query the store information corresponding to the database and then write it to Redis and set the logical expiration time.
    // 6.1. Obtain mutex lock
    boolean isLock = tryLock(LOCK_SHOP_KEY + id);
    // 6.2. Determine whether the lock acquisition is successful
    if (isLock){
        // Check again whether the Redis cache has expired (double check). If it exists, there is no need to rebuild the cache.
        // If the store information cached in Redis is still expired, start an independent thread to implement cache reconstruction (you can sleep for 200ms during testing), and the actual logical expiration time of the cache is set to 30 minutes.
        CACHE_REBUILD_EXECUTOR.submit( ()->{// Start an independent thread
            try{
                this.saveShop2Redis(id,20L);
            }catch (Exception e){
                throw new RuntimeException(e);
            }finally {
                unlock(LOCK_SHOP_KEY + id);
            }
        });
    }
    // 6.4. Return expired store information
    return shop;
}

public void saveShop2Redis(Long id, Long expirSeconds) {<!-- -->
    // 1. Query store data in the database according to the store ID
    Shop shop = getById(id);
    // Since local query database is faster, simulate reconstruction delay
    Thread.sleep(200);
    // 2. Encapsulate the logical expiration time (convert the current time to seconds)
    RedisData redisData = new RedisData();
    //Set hot store information
    redisData.setData(shop);
    redisData.setExpireTime(LocalDateTime.now().plusSeconds(expirSeconds));
    // 3. Convert the RedisData object containing hot store information and logical expiration time fields into a JSON string and cache it to Redis
    stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
}

Test logic expiration to solve cache breakdown

Use Jmeter to test whether all threads will perform cache reconstruction or return old data when they cannot find the data. If the data is inconsistent when rebuilding the data, will the cache data in Redis be updated

  • Use the saveShop2Redis method in the test class HmDianPingApplicationTests to add a cache of hot store information to Redis and set the logical expiration time to 2 seconds.
  • Manually modify the information of this hot store in the MySQL database. After 2 seconds, the hot store data cached in Redis will logically expire and be inconsistent with the corresponding store information in the MySQL database.
  • When users access expired cached data, they need to open a new thread to reconstruct the cached data. Before reconstruction, only dirty data (data before modification) can be obtained, and new data (data after modification) can be obtained after reconstruction. data)

Open 100 to access logical expired data

The users in the front can only see the dirty data, and the users in the back can see the new data

syntaxbug.com © 2021 All Rights Reserved.