在了解緩存雪崩、擊穿、穿透這三個問題前,我們需要知道為什么我們需要緩存。在了解這三個問題后,我們也必須知道使用Redis時,如何解決這些問題。
所以我將按照"為什么我們需要緩存"、"什么是緩存雪崩、擊穿、穿透"、"如何解決這些問題"三部分,帶你學懂緩存雪崩、擊穿、穿透。
為什么我們需要緩存
用戶的數(shù)據(jù)一般都是存儲于數(shù)據(jù)庫,數(shù)據(jù)庫的數(shù)據(jù)是落在磁盤上的,磁盤的讀寫速度可以說是計算機里最慢的硬件了。當用戶的請求都訪問數(shù)據(jù)庫的話,可想而知,我們整個系統(tǒng)的并發(fā)量肯定是比較低的,而且如果一旦并發(fā)量上來了,數(shù)據(jù)庫也很容易崩潰。
那我們可以怎么樣來解決這個問題呢?
-
我們可以多用幾臺機器,進行負載均衡,提高系統(tǒng)的并發(fā)量。
-
也可以加一個中間層(緩存),避免用戶直接訪問數(shù)據(jù)庫。
-
......
很顯然,使用緩存是比較簡單且經(jīng)濟的方案。其實,在現(xiàn)在的服務端開發(fā)中,緩存中間件已經(jīng)是我們所離不開的了。其中Redis就是比較著名的key-value內(nèi)存數(shù)據(jù)庫。故本文章基于Redis編寫。
什么是緩存雪崩、擊穿、穿透
緩存雪崩
通常我們?yōu)榱吮WC緩存中的數(shù)據(jù)與數(shù)據(jù)庫中的數(shù)據(jù)一致性,會給 Redis 里的數(shù)據(jù)設置過期時間,當緩存數(shù)據(jù)過期后,用戶訪問的數(shù)據(jù)如果不在緩存里,業(yè)務系統(tǒng)需要重新生成緩存,因此就會訪問數(shù)據(jù)庫,并將數(shù)據(jù)更新到 Redis 里,這樣后續(xù)請求都可以直接命中緩存。
這樣的流程乍一看很正確,沒有任何問題。實際上隱藏著一些問題,這樣做將可能導致有大量的key在同一時間失效,如果此時有大量的用戶請求,那就會去訪問數(shù)據(jù)庫,從而導致數(shù)據(jù)庫的壓力驟增,如果數(shù)據(jù)庫頂不住當前的壓力,則會導致宕機,進而引起一系列問題,最后導致系統(tǒng)崩潰,這就是緩存雪崩。
引發(fā)緩存雪崩有以下幾種可能:
- 大量key同時失效
- 充當緩存的Redis宕機了
緩存擊穿
我們的業(yè)務通常會有幾個數(shù)據(jù)會被頻繁地訪問,比如秒殺活動,這類被頻地訪問的數(shù)據(jù)被稱為熱點數(shù)據(jù)。如果熱點key在某個時間過期了,此時大量請求會打到數(shù)據(jù)庫上,數(shù)據(jù)庫很容易被擊穿,這就是緩存擊穿。
實際上緩存擊穿與緩存穿透都是key失效的問題,你也可以認為緩存擊穿是緩存穿透的子集。
緩存穿透
緩存雪崩和擊穿都是key失效或者Redis不可用的場景,緩存穿透與它們不同。緩存穿透是當用戶訪問的數(shù)據(jù),既不在緩存中,也不在數(shù)據(jù)庫中,導致請求在訪問緩存時,發(fā)現(xiàn)緩存缺失,再去訪問數(shù)據(jù)庫時,發(fā)現(xiàn)數(shù)據(jù)庫中也沒有要訪問的數(shù)據(jù),沒辦法構建緩存數(shù)據(jù),來服務后續(xù)的請求。那么當有大量這樣的請求到來時,數(shù)據(jù)庫的壓力驟增。
發(fā)生的原因有:
- 業(yè)務操作錯誤,緩存的數(shù)據(jù)或數(shù)據(jù)庫的數(shù)據(jù)被刪除,或者意外被用戶訪問到不存在的數(shù)據(jù)
- 黑客惡意攻擊
如何應對
緩存雪崩應對
緩存雪崩有兩種誘因,不同誘因應對的策略是不同的。
針對大量數(shù)據(jù)同時過期:
- 均勻設置過期時間
- 互斥鎖
- 后臺更新緩存
- ......
1.均勻設置過期時間
目的是要避免大量數(shù)據(jù)設置成同時過期。給這些設置了過期時間的key加上一個隨機數(shù),讓他們盡量不同時過期。尤其是在緩存預熱的時候,更需要這樣做。
2.互斥鎖
當業(yè)務線程在處理用戶請求時,如果發(fā)現(xiàn)訪問的數(shù)據(jù)不在Redis里,就加一個互斥鎖(setnx命令可以達到這個效果),保證同一個時間內(nèi)只有一個請求來構建緩存(從數(shù)據(jù)庫中讀,再更新到Redis),構建完后釋放鎖,未能獲取到鎖的請求,要么等鎖釋放后重新讀取緩存,要么返回空或默認值。
java
復制代碼
public class RedisTool { private static final String LOCK_SUCCESS = "OK"; private static final String SET_IF_NOT_EXIST = "NX"; private static final String SET_WITH_EXPIRE_TIME = "PX"; /** * 嘗試獲取分布式鎖 * @param jedis Redis客戶端 * @param lockKey 鎖 * @param requestId 請求標識 * @param expireTime 超期時間 * @return 是否獲取成功 */ public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) { String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime); if (LOCK_SUCCESS.equals(result)) { return true; } return false; } }
第一個為key,我們使用key來當鎖,因為key是唯一的。
第二個為value,我們傳的是requestId,很多童鞋可能不明白,有key作為鎖不就夠了嗎,為什么還要用到value?分布式鎖有一個條件,誰上的鎖就必須誰解開,通過給value賦值為requestId,我們就知道這把鎖是哪個請求加的了,在解鎖的時候就可以有依據(jù)。requestId可以使用多種方法生成,只要能保證在一段時間內(nèi)不重復。
第三個為nxxx,這個參數(shù)我們填的是NX,意思是SET IF NOT EXIST,即當key不存在時,我們進行set操作;若key已經(jīng)存在,則不做任何操作;
第四個為expx,這個參數(shù)我們傳的是PX,意思是我們要給這個key加一個過期的設置,具體時間由第五個參數(shù)決定。
第五個為time,與第四個參數(shù)相呼應,代表key的過期時間。
總的來說,執(zhí)行上面的set()方法就只會導致兩種結果:1. 當前沒有鎖(key不存在),那么就進行加鎖操作,并對鎖設置個有效期,同時value表示加鎖的客戶端。2. 已有鎖存在,不做任何操作。
不過這種方法就要注意一下死鎖問題,然后我代碼這樣寫的原因是,只有一個操作,是原子性的,如果把加鎖和設置過期時間分開,可能會發(fā)生一些意想不到的問題。
3.后臺更新緩存
業(yè)務線程不再負責更新緩存,緩存也不設置有效期,而是讓緩存“永久有效”,并將更新緩存的工作交由后臺線程定時更新。雖然不設置有效期,但是其實有一個邏輯過期的標識,一旦業(yè)務線程發(fā)現(xiàn)這個緩存過期,就把它交給后臺線程去處理。業(yè)務線程返回”過期值“。
這種方法適合用于對于緩存一致性要求不會特別嚴格的場景
針對Redis宕機:
- 服務熔斷或進行請求限流
- 構建Redis主從或集群來保證可靠
1.服務熔斷或進行請求限流
暫停業(yè)務,直接返回錯誤。等Redis恢復正常后,再允許業(yè)務進行。目的是保護數(shù)據(jù)庫。
也可以啟用限流,只允許少部分請求訪問數(shù)據(jù)庫,大于能承受的壓力的請求直接拒絕服務。等到Redis正常且預熱完畢,再解除限流。
2.構建Redis主從和哨兵或集群
主從能夠分擔主節(jié)點壓力,有了哨兵的話,能夠在Redis主節(jié)點故障時,即使切換主節(jié)點,避免Redis故障導致的緩存雪崩問題。
Redis集群(cluster)也是同理。
緩存擊穿應對
上面提到緩存擊穿是緩存雪崩的子集(數(shù)據(jù)過期導致的)
所以緩存擊穿的解決方法與因為數(shù)據(jù)過期導致的雪崩基本一致,可以采用:
- 互斥鎖方案(與上面講的相同)
- 后臺更新緩存方案(與上面講的相同)
緩存穿透應對
應對緩存穿透的方案,常見的方案有三種:
- 非法請求的限制
- 緩存空值或者默認值
- 使用布隆過濾器快速判斷數(shù)據(jù)是否存在,避免通過查詢數(shù)據(jù)庫來判斷數(shù)據(jù)是否存在
1.非法請求的限制
當不用訪問數(shù)據(jù)庫就能知道請求的數(shù)據(jù)是否合法時,這個方式很合適??梢灾苯釉?strong>API的入口處做判斷,避免非法請求訪問緩存和數(shù)據(jù)庫
2.緩存空值
當我們線上業(yè)務發(fā)現(xiàn)緩存穿透的現(xiàn)象時,可以針對查詢的數(shù)據(jù),在緩存中設置一個空值或者默認值,這樣后續(xù)請求就可以從緩存中讀取到空值或者默認值,返回給應用,而不會繼續(xù)查詢數(shù)據(jù)庫。
要注意緩存的空值必須設置合適的過期時間,太短則會導致緩存沒有阻擋住大多數(shù)的非法請求,太長則會導致浪費內(nèi)存空間。
3.使用布隆過濾器快速判斷數(shù)據(jù)是否存在,避免通過查詢數(shù)據(jù)庫來判斷數(shù)據(jù)是否存在
我們可以在寫入數(shù)據(jù)庫數(shù)據(jù)時,使用布隆過濾器做個標記,然后在用戶請求到來時,業(yè)務線程確認緩存失效后,可以通過查詢布隆過濾器快速判斷數(shù)據(jù)是否存在,如果不存在,就不用通過查詢數(shù)據(jù)庫來判斷數(shù)據(jù)是否存在。
可能有部分童鞋不了解布隆過濾器,我簡單的描述一下。
布隆過濾器由「初始值都為 0 的位圖數(shù)組」和「 N 個哈希函數(shù)」兩部分組成。當我們在寫入數(shù)據(jù)庫數(shù)據(jù)時,在布隆過濾器里做個標記(對N個哈希函數(shù)逐一進行使用,得到的結果對數(shù)組長度取余得到最后結果,并且把最后結果對應的下標值為1)。下次查詢數(shù)據(jù)在不在數(shù)據(jù)庫時,可以用相同的方法,如果得到N個位置的值都為1,則這個請求大概率是合法的(即使會有部分非法請求還是會訪問到數(shù)據(jù)庫,但是布隆過濾器已經(jīng)過濾了絕大多數(shù)了)
總結
在網(wǎng)上找到的這張表格對上述內(nèi)容進行了不錯的總結。文章來源:http://www.zghlxwxcb.cn/news/detail-464973.html
文章來源地址http://www.zghlxwxcb.cn/news/detail-464973.html
到了這里,關于學懂緩存雪崩,緩存擊穿,緩存穿透僅需一篇,基于Redis講解的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!