[原創]分佈式系統之緩存的微觀應用經驗談(四)【交互場景篇】

分佈式系統之緩存的微觀應用經驗談(四)【交互場景篇】
前言 
 
  近幾個月一直在忙些瑣事,幾乎年後都沒怎麼閒過。忙忙碌碌中就進入了2018年的秋天了,不得不感嘆時間總是如白駒過隙,也不知道收穫了什麼和失去了什麼。最近稍微休息,買了兩本與技術無關的書,其一是 Yann Martel 寫的《The High Mountains of Portugal》(葡萄牙的高山),發現閱讀此書是需要一些耐心的,對人生暗喻很深,也有足夠的留白,有興趣的朋友可以細品下。好了,下面迴歸正題,嘗試寫寫工作中緩存技術相關的一些實戰經驗和思考。
正文
 
  在分佈式Web程序設計中,解決高併發以及內部解耦的關鍵技術離不開緩存和隊列,而緩存角色類似計算機硬件中CPU的各級緩存。如今的業務規模稍大的互聯網項目,即使在最初beta版的開發上,都會進行預留設計。但是在諸多應用場景裏,也帶來了某些高成本的技術問題,需要細緻權衡。本系列主要圍繞分佈式系統中服務端緩存相關技術,也會結合朋友間的探討提及自己的思考細節。文中若有不妥之處,懇請指正。
  爲了方便獨立成文,原諒在內容排版上的一點點個人強迫症。

  第四篇打算作爲系列最後一篇,這裏嘗試談談緩存的一些併發交互場景,包括與數據庫(特指 RDBMS)交互,和一些獨立的高併發場景相關補充處理方案(若涉及具體應用同樣將主要以Redis舉例)。

  另見:分佈式系統之緩存的微觀應用經驗談(三)(數據分片和集羣篇)
    
  一、簡單談下緩存和數據庫的交互流程

    爲了便於後面的相關討論,這裏約定文中的數據庫(Database)均指傳統的 RDBMS,使用DB標識,同時需區別於緩存(Cache)裏的DB劃分空間。

    我在早前一篇緩存設計細節的文章裏有闡述關於 Cache 自身 CURD 時的一些具體細節,而這裏將結合DB,就 DB 和 Cache 之間的並行 CURD 操作進行一些討論。當然,這裏面在交互層面上是一定會涉及到分佈式事務(Distributed Transaction)相關的一致性話題,但爲了避免表述出現模糊和不必要的邊界放大,這裏我儘可能剝離開來,專注在基於 Cache 的處理上。

  預先抽象這樣一個基礎場景:DB中存在一張資金關聯表(FT),這裏 FT 裏存儲的都是熱點條目(屬於極高頻訪問數據),在系統設計時,FT裏的數據將與對應的 Cache 服務 C1 進行關聯存儲(這裏僅指一級緩存),以達到提升一定的併發查詢性能。
    1.1 向 FT 中新增(Create)一條數據
      通過 SQL 向 FT中插入一條數據:如果插入失敗,則不需要對 C1有任何操作;如果插入成功,則此時需要判斷,考慮是否在 C1中同步插入。
      這種情景一般比較簡單,如果沒有特別的情況,此刻不需對 C1 做主動插入,而是後續被動插入(後面會提到)。但是如果插入 FT 中的數據往後操作只有刪除這個動作,並且 FT的數據經常被批量操作,那麼個人建議 同步執行對 C1的插入操作。
      (PS:這裏也順便申明下,如果需要往C1插入,但插入失敗,請根據業務場景加入重試機制,後面對Cache的操作均包含這個潛在的動作。至於重試處理失敗的情況,如往C1插入一條數據,個人建議是不再過度處理,最終默認是整體操作成功,並進行對應狀態返回。這裏注意不要與分佈式事務的一致性進行混合類比,後面不再贅述。)
    1.2 準備更新(Update)一條數據
      當需要更新 FT 中的一條數據時,意味着之前 C1 中的數據已經無效,而在一個高併發環境中這裏無法做到統一的直接更新 C1。首先就需要考慮的是 C1 的數據是主動更新還是被動更新,主動更新即更新完 FT後,同時將數據覆蓋進 C1,而被動更新指的是更新完 FT 後,立即淘汰 C1 中的數據,並等待下次查詢時重新寫入C1。
      只要上述請求動作出現了任何併發,比如兩個相同動作,動作1和動作2同時發生請求,那麼會出現一個不一致的問題:動作1先操作 FT,動作2後操作 FT,然後動作2先操作了C1,動作1後操作了C1。
      這樣存在不止一個線程併發的更新 FT 數據時,無法確認更新 FT 的順序和最終更新 C1 的順序是否保持一致,結果是一定會出現大量 FT 和 C1 中數據出現幻讀,而這個在存在主從Cache的情況下這種概率會大大提升(可參見上一章主從複製的部分)。推薦的方式是,如果不考慮Cache 多次需要重寫的損耗,在沒有其他特殊要求下,可以直接淘汰 C1 中的數據,也額外照顧到了Cache在合適的時候完全命中(Hit)。
      其實到這裏還沒結束,當決定是淘汰 C1 的數據,那麼就要選擇一個淘汰時機:一種是先更新 FT,然後對C1 執行淘汰;一種則是,先對 C1 執行淘汰,然後才更新FT。
      雖然兩種方式都有合適的場景,但這裏需要權衡一種概率性問題:當對C1執行淘汰時,又併發了一個對C1的查詢操作,此時,C1會從DB拉取數據重新寫入,那麼C1中即爲髒數據,當併發越大,存在數據一直“髒”下去的概率更大。所以,這裏更推薦的做法是選擇前者。
      (注意,這裏還有一些去討論的細節並不打算在此話題延伸,比如關於 C1和FT之間的原子性問題,是否可以採用二階段/三階段提交等模擬事務方式和對業務造成的影響。
    1.3 開始讀取(Read)一條數據
      這裏就沒有太多特別,畢竟應用Cache 的目的就已經說明了讀取數據時,只需要遵循“先讀Cache再讀DB”。即先從C1裏拿取數據,如果C1裏不存在該數據,則從FT中搜索,搜索完成如果依然不存在該數據,則直接返回Empty狀態。如果存在,則同時將該數據保存進C1中,並返回對應狀態。
      順帶提一下,可能有人會說,在某些場景下,即使 C1中有數據,也要先從 FT裏優先獲取。我贊同,沒錯,但注意這裏不要混淆討論的主題了,這本質是屬於基於一種業務結果的導向,就類似在傳統 RDBMS 讀寫分離情況下,在關鍵數據的驗證處,直接請求主庫獲取並操作。所以上面說的其實並沒有矛盾,我們討論時要明確清晰,不要混淆。
    1.4 從FT 中刪除(Delete)一條數據
      與Create相反的操作,通過 SQL 向 FT中移除一條數據:如果移除失敗,則不需要對 C1 有任何操作,如刪除成功,則將對應C1中數據移除(另外請類比1.2中的一些細節)。
  二、談談緩存的穿透雪崩等相關問題
    在項目發展到後期,一些業務場景整體都處於高併發狀態,大量QPS對整體業務的負載要求很高,爲了避免很多時候脫離架構優化的初衷,還需要在項目中做到很多預先性的規避和細節把控。
    2.1 優化防止緩存擊穿
      當請求發來的查詢 Key 在 Cache 中存在,但某一時刻數據過期了,並且此時出現了大量併發請求,那麼這裏因爲 Cache 中 Miss,就會統一去 DB 中搜索,直接造成在很短的時間內,DB 的 QPS 壓力會陡增
      對於這種問題的預防和優化,往往從兩方面入手:一是程序中加小粒度的鎖/信號(去年有寫過一篇關於商城系統裏庫存併發管控雜記,裏面有具體話題的細節擴展,詳見:https://www.cnblogs.com/bsfz/ );二是將 DB的讀取延遲 和 Cache的寫入時間儘可能拉到最低;三是對其中過於熱點的數據採取一個較大的過期時間並做一定的隨機性(這裏非必要,可自行權衡)。其實還有一點,少數情況下,可根據場景是否限制,可以增加適當的到期自動刷新的策略,這裏也可以考慮在程序中開啓固定的線程通知維護。
    2.2 預防大量緩存穿透
      當請求發來的查詢 Key 在 Cache 中 Miss,自然就會去 DB 裏搜索,這裏本身沒問題,但是假如查詢的 Key 在 DB 中也不存在,那麼意味着每次請求實際上都是實打實落在了 DB 上。這種問題比較常見,並且即使併發不是很大的時候 DB 的連接數也輕鬆達到上限,而且本身也不符合我們設計爲了提高QPS的初衷。
      對於這種漏洞性問題的解決方式,同樣可以從兩方面入手:一是程序可以在第一次從DB搜索數據爲 NULL 的時候,直接將 NULL 或者一個標識符 Sign 緩存起來,同時個人建議儘量設置一個小範圍的隨機過期時間,避免不必要的長期內存佔用;二是程序裏限制過濾一些不可能存在的數據KEY,如借鑑 Bloom filter 思想,特別是在前端請求到後端的這裏,儘量進行一次中間判斷處理(如有時對不合法KEY直接返回NULL)。
    2.3 控制緩存雪崩
      這裏會有某些細節和上面類似,但不完全。當Cache出現不可用,再或者大量數據同一場景裏同一時刻失效,批量請求直接訪問DB,並且此刻也等同於沒有任何Cache措施了。
      爲了規避這種偏極端的問題,主要可以考慮從三個方面入手:一是增加完善Cache 的高可用機制,並最好有單獨的運維監控預警;二是類似上面針對Cache的時間再次作隨機,特別是包含預熱和批量的場景裏。(ps:你看很多地方都有類似設計來降低一定概率,個人在設計時,即使是項目初期階段的簡化版本里也會包含進去。);三是,在部分場景增加多級Cache,但是在很多時候會增加其他的問題(如多級之前的同步問題),所以個人推薦優先增加到二級即可,然後稍微調整下時間儘量不高於一級Cache。

結語
  由於個人能力和經驗均有限,自己也在持續學習和實踐,文中若有不妥之處,懇請指正。
  本系列告一段落,正好也要去忙一些事情,暫時可能不寫相關的東西了。

  個人目前備用地址:
    社區2:https://www.cnblogs.com/bsfz/

End.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章