3 緩存擊穿(Hotspot Invalid)
- 擊穿針對的是某一個key緩存
- 而雪崩是很多key
某key失效時,正好有高并發請求通路該key。
通常使用【緩存 + 過期時間】幫助我們加速接口通路速度,減少後端負載,同時保證功能的更新,一般情況下這種模式已基本滿足需求。
但若同時出現如下問題,可能對系統十分緻命:
-
熱點key,通路量非常大
比如秒殺時。
- 緩存的建構需要時間(可能是個複雜過程,例如複雜SQL、多次I/O、多個接口依賴)
于是就會導緻:
在緩存失效瞬間,有大量線程建構緩存,導緻後端負載加劇,甚至可能讓系統崩潰。

某些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
緩存為準
使用異步線程負責維護緩存的資料,定期或根據條件觸發更新,這樣就不會觸發更新。
參考