1 介紹
作者是互聯(lián)網(wǎng)一線研發(fā)負(fù)責(zé)人,所在業(yè)務(wù)也是業(yè)內(nèi)核心流量來源,經(jīng)常參與 <font color="#f00" > 業(yè)務(wù)預(yù)定、積分競拍、商品秒殺等工作。</font>
近期參與多場新員工的面試工作,經(jīng)常就 『超高并發(fā)場景下熱點數(shù)據(jù)』 可用性保障與候選人進行討論。
本文聚焦一些關(guān)鍵點技術(shù)進行討論,并總結(jié)一些熱點場景的處理經(jīng)驗。
2 業(yè)務(wù)基礎(chǔ)架構(gòu)簡圖(假設(shè))

3 超高并發(fā)下熱點數(shù)據(jù)的穩(wěn)定性保障
3.1 命題背景
1000w+請求同時投向后端,如果緩存未建立、失效,甚至緩存服務(wù)故障,就會透過緩存層直接投向數(shù)據(jù)庫。
可能會造成整體擊穿/雪崩,怎么破?
3.2 各種業(yè)務(wù)場景及應(yīng)對方案
3.2.1 規(guī)律性熱點數(shù)據(jù)預(yù)熱
無論是聚集式熱key,還是散列式熱key,只要是有一定規(guī)律性的,均可以做<font color="#f00" > 預(yù)熱</font>。
既然是熱Key,那就想辦法盡可能讓它不進入MySQL,就不會對數(shù)據(jù)庫造成傷害,。
這種場景最常見的就是對一些字典數(shù)據(jù)做預(yù)熱,因為他們不容易改變,修改頻次較低,但又很容易在高峰期被群蜂請求(突發(fā)式的批量請求)。
電商領(lǐng)域比如:<font color='#f00'> 商品種類、品牌類型、折扣規(guī)則。</font>
辦公/教學(xué)領(lǐng)域比如:<font color='#f00'>學(xué)校、年段、班級、學(xué)科、考試科目等。</font>
<font color="#f00" >一般來說如果10點是峰值期,那么可以預(yù)先在8~10點期間,可以逐漸的把大部分緩存建立起來。</font>如圖:

3.2.2 非規(guī)律性熱點數(shù)據(jù)預(yù)熱
Redis + 應(yīng)用層 加探測器,預(yù)判熱Key,并將探測到的熱Key進行預(yù)熱。
1、baidu實時熱搜

2. taobao商品排行

<font color='#f00'>這種額外的開銷就是有一個實時計算的獨立組件,因為熱點新聞、熱點數(shù)據(jù)都有急劇突變的特性。比如weibo多次因為突發(fā)熱點新聞導(dǎo)致網(wǎng)站崩潰。</font>
3.2.3 破解過期時間一致性問題
緩存的建立過程都是散列的,但是如果長時間靜待都會被逐漸釋放。
比如釘釘、飛書的辦公場景,遇到夜晚低峰期、周末節(jié)假日,緩存Key被逐步釋放之后。很容易在第二個工作日的早高峰造成大量創(chuàng)建緩存,流量井噴。
<font color='#f00'> 解決方案除了前面我們提到的緩存預(yù)熱之外,錯峰過期時間也是常規(guī)操作。</font>
可以給緩存設(shè)置過期時間時加上一個隨機值時間,使得每個key的過期時間分布開來,不會集中在同一時刻失效。
隨機值我們團隊的做法是:n * 3/4 + n * random() 。所以,比如你原本計劃對一個緩存建立的過期時間為8小時,那就是6小時 + 0~2小時的隨機值。
這樣保證了均勻分布在 6~8小時之間。如圖:

3.2.4 過濾垃圾請求
一般情況下,我們?nèi)?shù)先從緩存中Get Key,不存在的時候再從數(shù)據(jù)庫中去獲取,但這很容易給攻擊者提供漏洞。
<font color='#f00'>他可以瘋狂模擬一些不存在的Key,讓你進入數(shù)據(jù)庫去取數(shù),這樣就可以拖垮你的數(shù)據(jù)庫,實現(xiàn)擊潰你系統(tǒng)的目的。</font>
有效的辦法是在服務(wù)層先判斷這個Key的是否符合標(biāo)準(zhǔn)(比如滴滴的訂單數(shù)據(jù)緩存包含時間戳+用戶ID的序列化),這樣可以過濾一部分無效攻擊。
但是如果他能夠破解你key的規(guī)則,依舊可以鉆漏洞。你可以在緩存層上加一層過濾器,幫你Filter掉那些不合理的攻擊。
詳細(xì)可以參考我這篇《Redis系列16:聊聊布隆過濾器(原理篇) 》

3.2.5 消息隊列和削峰
如果一個緩存不存在(不存在、過期、被誤刪都有可能),但是同時有千萬請求投奔過來。
<font color='#f00'>這時候關(guān)心是不是及時拿回正確數(shù)據(jù)已經(jīng)不重要了,保住你的緩存和數(shù)據(jù)庫不被擊穿才是關(guān)鍵。</font>
隊列的目的是讓并行變成串行,這一定程度上降低系統(tǒng)處理用戶請求的吞吐能力,但是卻能很好的緩解你服務(wù)的壓力和風(fēng)險。

如上圖:第一個請求B從數(shù)據(jù)庫中取,后面的C、A就是從緩存服務(wù)中取了,壓力變小很多。
3.2.6 適當(dāng)加鎖
分布式鎖場景,在訪問key之前,采用SETNX(set if not exists)來設(shè)置另一個短期key來鎖住當(dāng)前key的訪問,訪問結(jié)束再刪除該短期key。
這種現(xiàn)象是多個線程同時去查詢數(shù)據(jù)庫的這條數(shù)據(jù),那么我們可以在第一個查詢數(shù)據(jù)的請求上使用一個 互斥鎖來鎖住它。
<font color='#f00'>其他的線程走到這一步拿不到鎖就等著,等第一個線程查詢到了數(shù)據(jù),然后做緩存。后面的線程進來發(fā)現(xiàn)已經(jīng)有緩存了,就直接走緩存。</font>
鎖不好的地方就是在其他線程在拿不到鎖的時候就等待,<font color='#f00'>這個會造成系統(tǒng)整體吞吐量降低,用戶體驗度也不好。</font>
<font color='#f00'>這算是一種簡單明了的降級策略了。</font>
3.2.7 限流策略
<font color='#f00'>一樣是一種在流量井噴時保住服務(wù)不雪崩的有效方法,限流一般是從服務(wù)層去實現(xiàn)的。</font>
Java服務(wù)的話可以使用 Hystrix進行限流 + 降級 ,比如一下子來了1W個請求,超過當(dāng)前系統(tǒng)的吞吐承受能力,假設(shè)單秒TPS的能力只能是 5000個,那么剩余的 5000 請求就可以走限流邏輯。
可以設(shè)置一些默認(rèn)值,然后調(diào)用我們自己降級邏輯去FallBack,保護最后的 MySQL 不會被大量的請求掛起。 除了Hystrix之外,阿里的Sentinel 和 Google的RateLimiter 都是不錯的選擇。
Sentinel 漏桶算法

RateLimiter 令牌桶算法

3.2.8 降級策略(備選緩存)
你的緩存層存在主備場景,他們之間定時異步同步,所以允許存在短暫數(shù)據(jù)不一致的情況。
當(dāng)你的主服務(wù)掛了之后,降級去讀備服務(wù),數(shù)據(jù)時效性沒那么高,但是也避免了數(shù)據(jù)庫被打穿的情況發(fā)生。

3.2.9 降級策略(客戶端緩存)
參考Redis 6.0的 Client Side Cache,看我這篇《追求性能極致:客戶端緩存帶來的革命》。
類似4.5做法,客戶端緩存時效性會差一點,畢竟存在訂閱跟同步的過程,數(shù)據(jù)沒那么新。但是避免大量的請求直接上緩存服務(wù),又因無效的緩存服務(wù)又把壓力轉(zhuǎn)移給數(shù)據(jù)庫。

3.2.10 降級策略之空初始值
這是一種短效的降級方式:
如果一個緩存失效的時候,有無數(shù)個請求狂奔而來,而第一個請求從進入緩存池,判空,再到數(shù)據(jù)庫檢索,再查詢出結(jié)果并返回設(shè)置緩存的這個過程里,緩存是不存在的。
這個就很危險,超高并發(fā)下這個短暫的過程足已讓千千萬萬請求投向數(shù)據(jù)庫。更別提這可能是個慢查詢,整個過程可能長達(dá)2s以上,那對數(shù)據(jù)庫是一種非常大的傷害。
業(yè)內(nèi)有一種做法叫做空初始值,短暫的局部降級來保證整個數(shù)據(jù)庫系統(tǒng)不被擊穿。大概流程如下:

可以看出,整個過程中我們犧牲了A、B、C、D的請求,他們拿回了一個空值或者默認(rèn)值,但是這局部的降級卻保證整個數(shù)據(jù)庫系統(tǒng)不被擁堵的請求擊穿。
3.2.11 高可用集群和自動擴縮容
集群模式和自動擴縮容模式從服務(wù)到緩存到數(shù)據(jù)層都應(yīng)該具備,否則無法根據(jù)流量來進行彈性伸縮,保持高可用。
如下圖, <font color="#6C8EBF" > 藍(lán)色部件</font>是擴容的部分,每一分層都有自己的動態(tài)擴容機制。

詳細(xì)可以參考筆者這幾篇文章。
《云原生:使用HPA和VPA實現(xiàn)集群擴縮容》
《數(shù)據(jù)庫系列:數(shù)據(jù)庫高可用及無損擴容》
3.2.12 雪崩之后的恢復(fù)
如果最終導(dǎo)致了緩存雪崩,那么重啟后快速的數(shù)據(jù)恢復(fù)也是我們核心的目標(biāo)。
剛剛恢復(fù)重啟的緩存服務(wù),這時候數(shù)據(jù)都是空的,大量的請求流量帶來的緩存重建(進而拉動數(shù)據(jù)庫流量)勢必會帶來壓力甚至二次雪崩。
這時候最好的辦法就是能夠有工具進行緩存恢復(fù),而不是從數(shù)據(jù)庫中去獲取數(shù)據(jù)來重建,這樣的過程漫長而負(fù)重。
這塊可以參考筆者的這兩篇文章:
《Redis系列:RDB內(nèi)存快照提供持久化能力》
《Redis穩(wěn)定性之戰(zhàn):AOF日志支撐數(shù)據(jù)持久化 》
4 總結(jié)
擴展閱讀:緩存雪崩、擊穿、穿透
《架構(gòu)與思維:一次緩存雪崩的災(zāi)難復(fù)盤 》
《架構(gòu)與思維:再聊緩存擊穿,面試是一場博弈》