一種基于“哨兵”的分布式緩存設計

AngBold 8年前發布 | 30K 次閱讀 緩存服務器

來自: http://blog.csdn.net//jiao_fuyou/article/details/46604697


14年雙11大促緩存方案,今天有點閑暇時間,回顧一下當時的思路。

場景介紹:

大促活動下,對于某些產品進行整點秒殺活動。預計流量是平時峰值5+倍

商品計算邏輯比較復雜:某個最終展示的商品屬性和價格,可能需要上億次動態條件計算獲得,動態條件每時每刻都在變化,并且商品的庫存屬性屬于行業共有庫存,每時每刻都在變化。

計算模型:前端機并發去后端獲取實時計算數據,然后合并結果,根據用戶信息給商品打屬性,排序。

頭腦風暴

針對這種場景,有很多方案可以嘗試,不過總結起來,大概倆個方向:擴容緩存

擴容

擴容是最容易想到的方式,而且每年大促,根據壓測和運營活動預期,都可能有相應擴容。擴容從某種程度上說,也是最簡單的方式,如果應用規劃足夠好,沒有狀態,那么基本不用開發介入就能完成。

但是如果應用涉及狀態信息,那么擴容就沒有說的那么輕巧,擴容涉及到增加集群狀態;活動結束后,機器下線涉及集群減少狀態,這一增一減,增加了運維的成本和系統穩定性。

擴容還有一個不好的地方就是活動結束后,系統水位下降,閑下來4倍的機器,比較浪費。

緩存

相對擴容,緩存是一種從應用角度出發,優化系統的方案。緩存的方案可細分不同粒度,分別適用不同場景。

靜態化

靜態化能最大限度降低最大限度降低后端壓力,一般靜態內容可以定時或者通過數據更像觸發生成,然后推送到CDN節點。靜態化適用于1)讀多寫少的數據,或者2)能夠容忍數據變化延遲的場景。對于本文介紹的場景,并不適合,原因在于商品不滿足前面說的兩點,并且每個登陸用戶看到的產品價格和屬性是不同的。

緩存中間結果

靜態化這種”一刀切”模式,不能滿足針對每個用戶的個性化展示需求,如果把每個用戶看到的數據都靜態化,那緩存的命中率有會很低,基本每個用戶請求一兩次就不會再來了。而且緩存數據量巨大。

由此想到把緩存粒度縮小,把緩存從展示層后推到前端機上。因為前端機負責匯總后端結算結果,并根據用戶信息給商品打屬性,排序。

緩存方案嘗試

經過頭腦風暴,最終確定采用緩存中間結果方式。接下來討論一下方案細節。

簡單粗暴方式

如果緩存有數據,取緩存數據,如果沒有,請求后端并把結果更新到緩存。這是一種最簡單的緩存模式,但不幸的是不適合秒殺場景,因為秒殺開始的時候,緩存很能沒有數據,請求會穿透到后端。

實時緩存,異步更新方式

實時請求數據來自緩存,緩存數據定時異步更新。粗看起來,這個方案不存在緩存穿透的情況,因為數據不會實時從后端計算獲取,而是從緩存獲取,如果緩存數據存在,直接獲取即可。緩存更新可以把用戶請求匯總后去重,定時更新。

上面討論的兩種方式都一個共性問題:第一批請求問題:如果第一批請求緩存沒有數據怎么辦?

簡單粗暴的方式會讓這樣的請求穿透緩存,后端去處理并更新緩存。這樣會給后端計算帶來壓力,秒殺開始那一剎那,很可能支撐不住。

實時緩存的犧牲了這樣的請求,因為這些請求根本看不到數據,所以請求失敗。這兩種方式在本文的應用場景都不合適。

為何不提前初始化緩存?

的確,上面兩個方案如果能在第一批請求到達前初始化好緩存,那基本上可以滿足本文的應用場景的。而且看起來也很容易做到,提前模擬一次請求或者提前往緩存放一份數據不就可以了嗎?

不幸的是,本文場景因為涉及數據范圍巨大,不能在較短時間內遍歷緩存key,初始化好緩存,即使采取并發方式。而且,初始化緩存請求過多,也將給后端機器造成壓力。

緩存失效又該怎么辦?

根據需求,兩種方案的緩存不會永久有效,如果緩存失敗了怎么辦?

對于簡單粗暴方式,如果緩存失效,又會遇到第一批請求問題,一批請求發現緩存失效,怎么辦?看來即使解決了緩存初始化問題,還有可能導致緩存穿透。

實時緩存模式也有類似問題,如果異步更新前數據已經失效了,那么將犧牲一批數據失效后到更新前這批用戶。因為沒有人去更新數據。

緩存更新問題

不管哪種方式,分布式緩存更新都存在并發問題,尤其在整點秒殺場景更為突出。對于簡單粗暴方式,可以采用分布式鎖解決:如果緩存穿透的一批請求只有一個會真正打到后端是不是就可以解決了?

實時緩存也有同樣的問題,只不過異步請求可以把一段時間內的重復請求合并成一個,從側面避免了并發問題。

更好的緩存方式

把上面的討論結合,可以得到一種更優雅的緩存方案,既不犧牲第一批請求,也不存在緩存穿透問題,同時避免并發更新問題。

哨兵

想象有這樣一個哨兵線程,只有它能去后端請求實時數據,并更新緩存。

第一批請求場景:

image

第一批請求中,選取最早的那個請求為哨兵,這個線程不會去讀緩存,直接去后端獲取計算結果并更新緩存。其他普通線程則自旋+sleep等待,直到哨兵更新緩存后,能拿到數據為止。

緩存失效場景:

image

哨兵的作用是讓緩存永不失效。哨兵線程提前蘇醒,去后端獲取計算數據并更新緩存。這樣,其他普通線程根本不會感知到緩存已經失效,他們能一直拿到最新的緩存。

例如,某個key的緩存失效時間的12:00:00,那么哨兵可能在11:59:55的時候蘇醒,請求后端并于11:59:57的時候完成緩存數據更新,后續請求線程感知不到數據的更新,一直能取到非過期的數據。

實現細節

哨兵:其實哨兵也是一個普通請求。可以用原子計數器(redis或tair)實現,一個數據有兩個key:原子計數器key和數據緩存key,二者緩存時間一致,但是計數器key失效時間比數據key的要早(至少提前一個后端請求RT時間,這樣能保證哨兵更新緩存后,不被其他線程感知到)。當請求線程發現緩存沒有數據的時候,每個線程去更新計數器,更新后,得到計數器為1的線程,被設置成哨兵線程,其他線程則等待哨兵。

普通請求沒有獲取到數據的時候,自旋+sleep應該有個超時時間,防止意外情況。如果超時了,根據業務場景選擇請求后端數據還是處理失敗。

 本文由用戶 AngBold 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!