redis緩存穿透、緩存擊穿、緩存雪崩的區(qū)別和解決方案

我們?cè)谝粋€(gè)比較大型的,流量和用戶(hù)都比較大項(xiàng)目中使用緩存,或者設(shè)計(jì)緩存系統(tǒng)時(shí),不得不要考慮的問(wèn)題就是:緩存穿透、緩存擊穿、緩存失效時(shí)的雪崩效應(yīng)。

我們一般的流程都是這樣的:前臺(tái)請(qǐng)求,后臺(tái)先從緩存中取數(shù)據(jù),如果取到數(shù)據(jù)就直接返回結(jié)果,取不到時(shí)就從數(shù)據(jù)庫(kù)中取,從數(shù)據(jù)庫(kù)中取到更新緩存,并返回結(jié)果,數(shù)據(jù)庫(kù)也取不到,那就直接返回空結(jié)果。

1、緩存穿透

描述:
緩存穿透是指緩存和數(shù)據(jù)中都沒(méi)有數(shù)據(jù),而用戶(hù)不斷發(fā)起請(qǐng)求。由于我們緩存邏輯一般都是不命中時(shí)被動(dòng)寫(xiě)的,并且出于容錯(cuò)考慮,如果從數(shù)據(jù)庫(kù)查不到數(shù)據(jù)庫(kù)就不寫(xiě)緩存直接返回,這將導(dǎo)致整個(gè)不存在的數(shù)據(jù)每次都要到數(shù)據(jù)庫(kù)去查詢(xún),這樣緩存就失去意義了。
在流量大時(shí),可能數(shù)據(jù)庫(kù)就會(huì)掛掉了,要是有人利用不存在的key頻繁攻擊我們的應(yīng)用,這就形成一個(gè)漏洞了。如發(fā)起id為小于0的數(shù)據(jù)或者id為特別大不存在的數(shù)據(jù),這時(shí)的用戶(hù)很可能是攻擊者,攻擊會(huì)導(dǎo)致數(shù)據(jù)庫(kù)壓力過(guò)大。

針對(duì)上面情況的解決方案

  • 最基本的接口層增加校驗(yàn),比如用戶(hù)鑒權(quán),一些ID做基礎(chǔ)的校驗(yàn),小于0或者大于某個(gè)范圍的值直接攔截

-從緩存取不到數(shù)據(jù),在數(shù)據(jù)庫(kù)中也沒(méi)有取到時(shí),將該緩存key的值設(shè)為null,緩存的有效時(shí)間可以設(shè)置短一點(diǎn),10秒,20秒(不宜過(guò)大,過(guò)大可能會(huì)導(dǎo)致正常情況也沒(méi)法使用)。這樣可以防止攻擊用戶(hù)反復(fù)攻擊同一個(gè)數(shù)據(jù)。

緩存擊穿

描述
緩存擊穿是指緩存中沒(méi)有但數(shù)據(jù)庫(kù)中有的數(shù)據(jù)(一般是緩存時(shí)間到期),這時(shí)由于并發(fā)用戶(hù)特別多,同時(shí)讀緩存沒(méi)讀到數(shù)據(jù)庫(kù),又同時(shí)去數(shù)據(jù)庫(kù)去取數(shù)據(jù),引起數(shù)據(jù)庫(kù)壓力瞬間增大,造成DB過(guò)大的壓力。
解決方案:
-一些熱點(diǎn)數(shù)據(jù)設(shè)置永遠(yuǎn)不過(guò)期
-接口限流與熔斷和降級(jí)。重要的接口一定要做好限流策略,防止用戶(hù)惡意刷接口,同時(shí)要降級(jí)準(zhǔn)備,當(dāng)接口中的某些服務(wù)不可用時(shí),進(jìn)行熔斷,失敗快速返回機(jī)制。
-布隆過(guò)濾器。bloomfilter就類(lèi)似于一個(gè)hash set,用于快速判斷某個(gè)元素是否存在于集合中,其典型的應(yīng)用場(chǎng)景就是快速判斷一個(gè)key是否存在,不存在就直接返回。布隆過(guò)濾器的關(guān)鍵就在于hash算法和容器大小。
-加互斥鎖 參考代碼如下

public function getCachedData($key)
  {
    $rs = Redis::get($key);
    if (empty($rs)) {
      $lock = 'list_lock_key';
      $lockResult = Redis::setnx($lock, true);
      if ($lockResult) {
          Redis::set($lock, true, 30);
          $rs = mysqlGetDate();
          Redis::delete($lock);
      }         
      if (!$lockResult) {
          usleep(100);
          $rs = $this->getCachedData($key);
      }
}   
    return $rs;
}
//緩存中沒(méi)有數(shù)據(jù),第1個(gè)進(jìn)入的線程,獲取鎖并從數(shù)據(jù)庫(kù)去取數(shù)據(jù),沒(méi)釋放鎖之前,其他并行進(jìn)入的線程會(huì)等待100ms,再重新去緩存取數(shù)據(jù)。這樣就防止都去數(shù)據(jù)庫(kù)重復(fù)取數(shù)據(jù),重復(fù)往緩存中更新數(shù)據(jù)情況出現(xiàn)

細(xì)心的朋友可能已經(jīng)發(fā)現(xiàn)上面代碼加鎖是有問(wèn)題的。問(wèn)題出在:假如我們獲取鎖成功后,程序突然崩潰了,那鎖不就是沒(méi)有釋放,后面的所有獲取鎖的請(qǐng)求都等待了。那就是出大問(wèn)題了。為了避免出現(xiàn)這樣問(wèn)題,我們來(lái)把代碼邏輯修改下

/**
進(jìn)程1獲得鎖后操作超時(shí)/崩潰/刪除鎖失敗,
進(jìn)程2檢測(cè)到鎖已存在,但獲取鎖的值對(duì)比當(dāng)前時(shí)間發(fā)現(xiàn)鎖已過(guò)期,

進(jìn)程2通過(guò)GETSET命令重新給鎖賦予新的值,并獲取到的鎖的舊值,再次對(duì)比鎖的舊值與當(dāng)前時(shí)間,如果鎖的舊值依然小于當(dāng)前時(shí)間的話,這時(shí)進(jìn)程2就可以忽略進(jìn)程1余留下的廢鎖進(jìn)行下步操作了

進(jìn)程2完成下步操作后返回前應(yīng)該刪除鎖,但在刪除鎖時(shí)可以先檢測(cè)鎖是否還未過(guò)期,未過(guò)期才做刪除操作,已過(guò)期的就沒(méi)必要在去刪除鎖了,因?yàn)楹苡锌赡芷渌M(jìn)程檢測(cè)到鎖過(guò)期時(shí)已經(jīng)去獲取鎖了

這里要重點(diǎn)說(shuō)明的是,如果有其他進(jìn)程在進(jìn)程2之前獲取到鎖,那么進(jìn)程2將獲取鎖失敗,但是進(jìn)程2在用GETSET獲取鎖的舊值時(shí)也賦予了鎖新的值,改寫(xiě)了其他進(jìn)程賦予鎖的超時(shí)值。
看到這大家可能會(huì)有疑問(wèn)了,進(jìn)程2沒(méi)獲取到鎖怎么能改變鎖的值呢?是的,進(jìn)程2改變了鎖的原有值,但這一點(diǎn)小小的時(shí)間誤差帶來(lái)的影響是可以忽略。
畢竟沒(méi)有完美的解決方案,時(shí)間換空間或者空間時(shí)間同理
**/

public function getCachedData($key)
  {
    $rs = Redis::get($key);
    if (empty($rs)) {
      $time = time();
      $lockExpire = 20;//鎖的過(guò)期時(shí)間
      $lockValue = $time + $lockExpire;//鎖的值
      $lock = 'list:'.$key;
      $lockResult = Redis::setnx($lock, $lockValue);
      if ($lockResult || Redis::get($lock)< $time || Redis::getset($lock, $lockValue) < $time) {
          Redis::expire($lock, $lockExpire);
          $rs = mysqlGetDate();

          //過(guò)期的鎖不用刪除
          if(Redis::ttl($lock))
            Redis::delete($lock);
      }         
      if (!$lockResult) {
          usleep(100);
          $rs = $this->getCachedData($key);
      }
}   
    return $rs;
}
//

緩存雪崩

描述
緩存雪崩是指緩存中數(shù)據(jù)大批量到過(guò)期時(shí)間,而且查詢(xún)數(shù)據(jù)量巨大,引起的數(shù)據(jù)庫(kù)壓力過(guò)大甚至宕機(jī)。和緩存擊穿不同的是,緩存擊穿是指并發(fā)查同一條數(shù)據(jù),緩存雪崩是不同數(shù)據(jù)都過(guò)期了,很多數(shù)據(jù)都從數(shù)據(jù)庫(kù)查。

解決方案
-緩存數(shù)據(jù)的過(guò)期時(shí)間設(shè)置隨機(jī),防止同一時(shí)間大量數(shù)據(jù)過(guò)期現(xiàn)象發(fā)生。
-如果緩存數(shù)據(jù)庫(kù)是分布式部署,將熱點(diǎn)數(shù)據(jù)均勻分布在不同的緩存數(shù)據(jù)庫(kù)中
-設(shè)置熱點(diǎn)數(shù)據(jù)不過(guò)期

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容