天天看點

Redis緩存雪崩、緩存穿透、緩存擊穿解決方案詳解(下)3 緩存擊穿(Hotspot Invalid)

3 緩存擊穿(Hotspot Invalid)

  • 擊穿針對的是某一個key緩存
  • 而雪崩是很多key

某key失效時,正好有高并發請求通路該key。

通常使用【緩存 + 過期時間】幫助我們加速接口通路速度,減少後端負載,同時保證功能的更新,一般情況下這種模式已基本滿足需求。

但若同時出現如下問題,可能對系統十分緻命:

  • 熱點key,通路量非常大

    比如秒殺時。

  • 緩存的建構需要時間(可能是個複雜過程,例如複雜SQL、多次I/O、多個接口依賴)

于是就會導緻:

在緩存失效瞬間,有大量線程建構緩存,導緻後端負載加劇,甚至可能讓系統崩潰。

Redis緩存雪崩、緩存穿透、緩存擊穿解決方案詳解(下)3 緩存擊穿(Hotspot Invalid)

某些Key屬極端熱點資料,并發量很大情況下,如果這個Key過期,可能會在某個瞬間出現大量的并發請求同時回源,相當于大量的并發請求直接打到了資料庫。這就是緩存擊穿或緩存并發問題。

解決方案

考慮使用鎖限制回源的并發。

如下代碼示例,使用Redisson來擷取一個基于Redis的分布式鎖,在查詢DB前先嘗試擷取鎖:

@Autowired
private RedissonClient redissonClient;
@GetMapping("right")
public String right() {
    String data = stringRedisTemplate.opsForValue().get("hotsopt");
    if (StringUtils.isEmpty(data)) {
        RLock locker = redissonClient.getLock("locker");
        // 擷取分布式鎖
        if (locker.tryLock()) {
            try {
                data = stringRedisTemplate.opsForValue().get("hotsopt");
                // 雙重檢查,因為可能已經有一個B線程過了第一次判斷,在等鎖,然後A線程已經把資料寫入了Redis中
                if (StringUtils.isEmpty(data)) {
                    // 回源到資料庫查詢
                    data = getExpensiveData();
                    stringRedisTemplate.opsForValue().set("hotsopt", data, 5, TimeUnit.SECONDS);
                }
            } finally {
                // 别忘記釋放,另外注意寫法,擷取鎖後整段代碼try+finally,確定unlock萬無一失
                locker.unlock();
            }
        }
    }
    return data;
}
      

這樣,可以把回源到資料庫的并發限制在1。

在真實的業務場景下,不一定要這麼嚴格地使用雙重檢查分布式鎖進行全局的并發限制,因為這樣雖然可以把資料庫回源并發降到最低,但也限制了緩存失效時的并發。

是以可以考慮:

使用程序内的鎖進行限制

這樣每個節點都可以以一個并發回源DB。

Semaphore

限制并發數,比如限制為10,這樣既限制了回源并發數不至于太大,又能使得一定量的線程可以同時回源。

永不過期

從 redis 上看,确實沒有設定過期時間。這就保證不會出現熱點 key 過期,即 “實體” 不過期。

“邏輯” 過期

功能上看,若不過期,不就成靜态資料了?

是以我們把過期時間存在 key 對應的 value。若發現要過期了,通過一個背景異步線程進行緩存建構,即 “邏輯” 過期。

服務降級

hystrix

緩存為準

使用異步線程負責維護緩存的資料,定期或根據條件觸發更新,這樣就不會觸發更新。

參考