日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網為廣大站長提供免費收錄網站服務,提交前請做好本站友鏈:【 網站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(50元/站),

點擊這里在線咨詢客服
新站提交
  • 網站:52003
  • 待審:43
  • 小程序:12
  • 文章:1047590
  • 會員:762

哈嘍,大家好呀,我是呼嚕嚕,最近很忙好久沒更新了,今天我們通過緩存與數據庫之間的一致性這個老生常談的問題來切入,聊聊如何合理的設計一個緩存系統?

如今互聯網應用,無論是web還是App,都基本遵循"前端-后端-數據庫"的架構模型

圖片當業務處于起步階段,流量比較小的時候,上述能夠支撐;但隨著業務的擴張,用戶數和流量越來越大,也就需要整個架構支撐起更大的并發量,但我們服務器上的資源總是有限的,當每天流量達到高峰時,往往這個時候數據庫最先頂不住

當我們分析這些互聯網應用的流量時候,發現大部分的流量實際上都是讀請求,而且大部分數據并沒有頻繁被改變**(即讀多寫少場景,注意本文全文討論的方案都是基于這個前提**)。這個時候引入緩存,是提升性能的一種行之有效的方式,緩存在計算機的世界中處處可見,比如CPU緩存,瀏覽器緩存,操作系統緩存,程序代碼中自定義緩存

由于數據庫每秒能接受的請求次數QPS是有限的,當我們在數據庫前面,引入緩存來充當緩沖層;如果命中緩存就直接獲取目標數據并返回,不僅能減少對數據庫的直接訪問帶來的計算壓力,還能提升響應速度,充分壓榨有效的資源,其本質是額外消耗更高速的空間來換時間

圖片圖片

凡是有利有弊,引入緩存后,享受緩存帶來的種種好處的優點,但緩存系統其實是非常復雜的,緩存和數據庫的一致性也是個繞不開,讓人腦闊疼的問題;還需要考慮緩存的穩定性、命中率、熱點數據、過期時間等等,我們下文慢慢道來

本地緩存、分布式緩存

緩存有各種分類,常見的是與應用耦合程度劃分為:本地緩存local cache和分布式緩存remote cache

本地緩存

本地緩存,由于存在于應用程序的本地內存,應用和緩存在同一個進程內,且沒有網絡延遲,所以速度快

但本地緩存的大小通常受到物理內存的限制,而且還要兼顧應用程序正常運行,容量有限,擴展性差,無法輕松擴展到多個節點。還有就是多個應用實例下無法直接的共享緩存,數據的一致性難以保證,復雜度高。數據會隨著應用程序的重啟而丟失

適合讀寫密集、對數據一致性要求較低、網絡環境不穩定的場景

分布式緩存

主要是指與應用分離的獨立緩存組件,比如redis,可擴展性強,容量大,可以通過集群水平擴展;通過通過一致性哈希等技術,保證多節點之間的數據一致性,而且都集成好了,開發者一般直接使用這些特性

當然由于存在網絡延遲,與本地緩存相比,速度較慢;硬件成本也需要較高,來保證其高可用、高可靠性

更適合電商平臺、社交網絡等流量并發大的平臺,或者互聯網這種隨著業務增長,需要彈性擴展以滿足需求的場景

還有綜合二者特點的多級緩存,將本地緩存和分布式緩存結合起來,本地緩存作為一級緩存,存儲更新頻率低,訪問頻率高數據;分布式緩存作為二級緩存,存儲更新頻率很高的數據

當用戶獲取數據時,先從一級緩存中獲取數據,如果一級緩存有數據則返回數據,否則從二級緩存中獲取數據。如果二級緩存中有數據則更新一級緩存,然后將數據返回客戶端。如果二級緩存沒有數據則去數據庫查詢數據,然后更新二級緩存,接著再更新一級緩存,最后將數據返回給客戶端。這里邏輯其實和CPU內部的緩存很像,大家感興趣地可以自行查閱筆者之前的一篇文章-CPU緩存

但緩存相關的問題邏輯挑戰,無論本地緩存還是分布式緩存都是一樣的,為方便起見,本文將全文以redis為例,來代稱緩存

緩存穿透、緩存擊穿、緩存雪崩

在將緩存和數據庫的一致性之前,我們需要保證,引入的緩存,即構建的緩存系統是穩定的,這是保證數據一致性的前提

關于緩存的穩定性,有3種經典問題:緩存穿透、緩存擊穿、緩存雪崩,聊這3個問題前,我們得知曉緩存最常見的應用模式Cache-Aside Pattern旁路緩存的讀模式:

圖片圖片

旁路緩存模式,是指優先查詢緩存,查詢不到再去查詢數據庫。如果這時候數據庫查到數據了,就將緩存的數據回寫更新,這樣緩存可以為后續請求服務!

緩存穿透

緩存穿透: 當請求過來,訪問不存在的數據時(即既不在緩存中,也不在數據庫中),這會導致訪問緩存,未命中,繼續訪問數據庫db,然后發現在數據庫中還是未查詢到數據,這個時候也就不能回寫緩存,來為后續的請求服務;也就是說,當這種請求過來,每次都會去查數據庫,緩存形同虛設,一旦流量暴增,容易直接帶崩數據庫

圖片圖片

這種不存在的數據可能被管理員誤刪,也有可能被黑客惡意利用(惡意請求),不斷地去試,一旦發現一個不存在的數據,就拼命發請求訪問這個數據,直到數據庫鎖住

那解決辦法也很簡單,常見的有:

  1. 比如每次訪問數據如果既不在緩存中,也不在數據庫中,那就緩存一個占位符或者空值,過期時間也不要設置過長,比如1分鐘就行,這樣的話,在1分鐘內,這么多請求只有一次能直接訪問數據庫,這樣就能顯著降低數據庫的壓力;如果緩存過期時間過長,會出現大量的空緩存,進而導致緩存資源的浪費
  2. 還可以針對請求攜帶的參數,比如是那種特殊字符、非法字符等,我們數據庫肯定不會存這些東西,直接在應用服務層進行限制,不允許訪問
  3. 還可以通過第三方組件來實現,比如布隆過濾器,其主要是其特性:布隆過濾器判斷一個元素不在集合中,那肯定就不在。如果判斷存在,那有一定可能性它在說謊,具體原理可以參考筆者以前的一篇文章海量數據處理的利器-布隆過濾器。在緩存和數據庫之間再加上布隆過濾器,通過布隆過濾器快速判斷數據是否存在,從而避免多次之間請求數據庫

緩存擊穿

在我們正常的業務之中,總有一些數據會被頻繁訪問,這就是熱點數據

所謂的緩存擊穿指的是,緩存中熱點數據的key過期失效,由于是熱點數據,在過期的一瞬間會有大量的請求過來(高并發),這些請求,最終都會直接訪問數據庫,這樣數據庫很容易被打垮,緩存仿佛被"擊穿"了

常見的解決方案:

  1. 加鎖,進程鎖/分布式鎖,當請求過來時,緩存未命中時,會通過鎖將這個緩存key鎖上,等當這個請求從數據庫獲取數據后再回寫到緩存中后,再釋放鎖;期間其他請求過來,會獲取鎖失敗,等待一段時間重試,就可以直接讀取緩存了。需要注意的是,如果業務量不大,進程鎖就夠了的話,也就沒必要上分布式鎖,多引入額外組件,就會增加系統的不穩定性

圖片圖片

還可以繼續改進,將請求2未獲得鎖,直接返回,升級成自旋鎖,它不直接返回,而是等待一會重新嘗試獲取鎖,這種高并發情況下,只有唯一請求是db請求,所有請求共享結果

  1. 給緩存的Key設置合理的過期時間并加上隨機值,盡量減少緩存短期大量失效,出現大量訪問數據庫的情況,實現"削峰填谷"
  2. 網上有文章提出,可以讓熱點數據的緩存不設置過期時間,這樣不就可以永不過期嘛,但這其實是個很危險的操作

使用緩存的前提是一定要設置過期時間,因為由于項目會不斷迭代更新,業務不斷復雜,開發人員更替,緩存會變得越來越難以維護,另外緩存和數據庫無法避免的數據不一致的情況,緩存的過期時間其實就是兜底,防止緩存和數據庫數據長時間不一致

我們還可以通過消息隊列來間接地讓熱點數據的緩存延期,當熱點緩存過期時,后臺服務再檢測更新緩存,防止緩存擊穿;至于是否延期,得做訪問量分析與統計,當然引入新的組件也會帶來額外的穩定性問題,還是得根據業務情況,實事求是

緩存雪崩

緩存雪崩,指定是大量請求未命中緩存,直接訪問數據庫,導致數據庫壓力過大,倘若請求足夠的多,會直接將數據庫壓垮,繼而影響整個系統,如同"雪崩"

個人感覺緩存擊穿是緩存雪崩的一個子集,緩存雪崩一般有2種誘因:緩存服務異常,比如redis故障宕機或者緩存服務是正常的,但大量緩存數據在同一時間過期

一般解決redis故障宕機,是搭建集群,由單節點到多節點,提升redis的容災能力,當主節點宕機后,從節點可以切換成為主節點,繼續提供緩存服務;若是真的宕機了,那我們應該使用熔斷機制,同時當流量到達一定的閾值,直接禁止請求對數據庫的訪問,返回系統擁擠之類的提示,維持系統穩定,等待緩存恢復再允許對數據庫訪問

防止大量緩存數據在同一時間過期,一般是給緩存的Key設置合理的過期時間并加上隨機偏差,盡量讓緩存失效時間均勻分布,實現"削峰填谷",簡單而有效

圖片圖片

要么加鎖,唯一db請求,所有同類請求共享結果,與緩存擊穿的解決方法一致,我們就不再贅述了

還有一種方式就是,當每天系統訪問的流量高峰來臨之前,先提前將熱點數據入緩存,避免直到用戶請求的時候,再先查詢數據庫,然后將數據緩存的過程,這個也叫緩存預熱

CAP原則 和 如何保證緩存一致性

由于在數據庫層前,引入緩存,主要是通過空間去換時間,享受緩存帶來的種種好處的優點,但此時一份數據存在不同的副本,且在不同空間中,此時更新緩存、db就會帶來緩存一致性的挑戰

我們還需要了解一下著名的CAP原則,指在一個分布式系統中,一致性Consistency、可用性AvAIlability、分區容錯性Partition tolerance,這3者最多同時滿足2項,不可能同時滿足3項!!!

圖片圖片

  1. 一致性Consistency,即所有節點在同一時間具有相同的數據,強一致性
  2. 可用性Availability,即服務必須一直處于可用的狀態,每次請求都能獲取到正常的響應,高可用
  3. 分區容錯性Partition tolerance,即分區故障時,要求在一定時限內,仍然或者恢復到能對外提供滿足一致性和可用性的服務,系統繼續正常運行

還記得本文的一開始嗎?

為了應對高流量,我們的系統選擇了高性能和高吞吐量,所以只能滿足AP。

而緩存與數據庫的緩存一致性難以避免的具體原因是:由于無法保證同時更新db和緩存不在同一個事務中,所以其不是原子操作,緩存不一致是無法避免的!

圖片圖片

要保證強一致性,我們可以上分布式鎖,但會導致整個系統的并發性能下降,還記得我們引入緩存的初衷嗎?是為了提升系統的整體性能吶!!!所以這種方案我們一般不采用~

但我們可以通過一些方案,來實現緩存的最終一致性,其次盡可能減小緩存不一致的時間窗口,我們下面分別來聊聊常見的幾種方式及其它們的問題:

  1. 先更新數據庫,再更新緩存
  2. 先更新緩存,再更新數據庫
  3. 先刪緩存,再更新數據庫
  4. 先更新數據庫,再刪除緩存

先更新數據庫,再更新緩存

先更新數據庫,再更新緩存,可能會遇到下面這種情況:

圖片圖片

當請求(或者可以說線程)并發的情況,比如2個請求1、2同時去更新db時,請求1快一點;但當程序延遲或者其他情況,導致當請求去更新緩存時,請求2快一點,這就會導致最終db=20,緩存=10這種數據不一致的情況,不一致的情況將持續到下次緩存失效,或者去更新數據庫緩存的時候,在此期間還不能保證更新緩存一定就可以成功

先更新緩存,再更新數據庫

這種和先更新數據庫,再更新緩存是類似的情況:

圖片圖片

這種更新緩存的方式,是無法避免并發導致的數據不一致問題,而且出現的頻率也不低,所以我們應該盡量不更新緩存。

先刪緩存,再更新數據庫 和 延遲雙刪

前一個更新請求,先刪除緩存,再更新數據庫,當后面讀請求來發現沒有命中緩存,去數據庫讀數據,然后再回寫到緩存中,給后續請求服務,這是個很不錯的設想,但它還是會出現下面這種情況:

圖片圖片

當2個并發請求過來,請求1是更新請求,當請求1刪除調緩存后,還沒去db更新數據,期間請求2來獲取數據,緩存未命中(剛被請求1刪了嘛),去數據庫獲取數據10后,后回寫緩存,把緩存更新為10;這個時候請求1終于去更新db了,把db更新為20,這個時候還是會出現緩存和數據庫不一致的情況

一旦發生數據不一致,臟數據會一直在緩存中,直到下一次更新請求過來

補充:延遲雙刪關注我,我再多講幾句~如今在先刪緩存,再更新數據庫的基礎上,還有個優化版叫延遲雙刪

既然請求可能會把臟數據重新寫入緩存中,臟數據會一直在緩存中,直到下一次更新請求過來,這個數據不一致的時間窗口較長,如果這個時候休眠指定時間N,我們另起線程(異步化)去刪除這個臟數據緩存,這個時候不就能縮短極端情況下不一致的時間窗口了嘛,一般N設為5s左右,需要根據項目實際情況而定。

另外也可以通過消息隊列MQ來刪除緩存,利用消息隊列的可靠性,來保證刪除緩存的操作能夠成功執行,并異步化進行復雜邏輯的解耦

先更新數據庫,再刪除緩存

那先更新數據庫,再刪除緩存呢?它也被稱為Cache Aside Pattern旁路緩存的寫模式,我們再來看一種情況:

圖片圖片

從上面時序圖,我們可以看出,先更新數據庫,再刪除緩存這種方案是可以保證緩存的最終一致性,但它在某一時間內,還是存在緩存不一致的時間窗口(上圖請求2命中緩存與數據庫不一致)

但這個不一致的時間窗口很短,通常不超過1ms,在互聯網項目中通常可以忽略這么短時間的不一致

但你覺得這就是終極方案了?

別急我們再看它有可能發生的一種情況:

圖片圖片

當2個并發請求過來,請求1是讀請求,正好緩存不存在,直接讀取db=20,在回寫緩存期間,請求2又過來更新db=10,在刪除緩存(沒緩存),然后請求1再姍姍來遲地更新緩存=20,這就導致了緩存與數據的不一致情況

但實際上這種情況,觸發的概率非常低,因為緩存的存取速度(內存),要遠遠快于數據庫(磁盤)。關于儲存介質的速度差異,感興趣地可以去看看計算機儲存器的讀寫速度差異

所以很難出現請求1已經更新了數據庫并且刪除了緩存,請求2才更新完緩存的情況;為防止刪除緩存失敗,給緩存加個過期時間簡單而有效

但這其實也反映了:

  1. 先更新數據庫,再刪除緩存這種模式并不太適合寫請求遠遠多于讀請求的場景下,而且當并發量特別高的情況下,緩存刪除的代價也會較大(容易緩存擊穿),這個時候更新數據庫后更新緩存可能是更適合的方案,還能進而通過MQ異步來優化
  2. 如果讀請求遠遠大于寫請求的場景下,先更新數據庫,再刪除緩存是個較好的方案,背后是lazy計算的思想:不要每次都重新做復雜的計算,而是等到它需要用的時候再重新計算
  3. 本文提到的這4種方案,無論是哪種方案都是無法絕對保證緩存的一致性,只能保證最終一致性,縮短不一致的時間窗口。所以緩存必須要設置過期時間,這就是對緩存不一致的兜底措施
  4. 最后如果對數據一致性要求極高的話,就不要再額外引入緩存,不引入緩存就沒有這么多煩惱!

如何保證刪除緩存能執行成功

另外在實際環境中,執行刪除緩存,也會有問題,因為無法保證系統會一定去刪除緩存,如果刪除緩存失敗,也會造成緩存與數據庫的不一致,下面介紹幾種常見的方案:

基于消息隊列刪除緩存

由于刪除緩存不一定能成功,一般會采用多次重試刪除的方案,需要一個隊列來記錄,是否刪除成功,如果沒有成功就繼續回隊列中,一般會引入中間件消息隊列MQ來,利用其高可靠性來保證刪除操作的執行,同時還能異步化,實現復雜業務邏輯的解耦

我們來看下其主要流程:

更新數據庫的同時,發送刪除緩存的消息到消息隊列中,首次消費消息去執行刪除緩存的操作,如果成功就直接返回業務,并把這個消息消費掉;如果由于各種原因導致緩存刪除失敗,那就重新將這個消息放進消息隊列中,等待下一次的消費

當第二次消費刪除該緩存的消息時,如果刪除成功就把該消息消費掉,并返回;如果沒有刪除成功就繼續放回消息隊列中,每個消息都有消費次數的上限,超出就報錯告警

圖片圖片

另外一般將更新數據庫的模塊和同時發生刪除緩存消息的模塊放在同一個服務里,因為這樣后期維護起來,才不會發現莫名奇妙,不然就是給排查和維護上強度~~

當然再引入mq,也要額外考慮mq的高可用性,所以需要根據實際情況,考慮是否有必要引入mq,如果不引入怎么辦?最簡單的我們可以通過內存隊列、線程池等方式實現,性能更高,畢竟在本地沒有網絡延遲,代價就是更考驗程序員的心智,啥都要操心~

基于binlog來刪除緩存

還有一種比較有意思的方式,我們上面需要在程序中顯式去發送消息,講人話就是程序需要額外承擔發送消息的壓力, 而通過訂閱數據庫比如MySQL的binlog,來監聽數據的真實變化來直接去處理有關的緩存,讓程序專心地去操作數據庫

binlog用于記錄數據庫執行的寫入性操作(不包括查詢)信息,以二進制的形式保存在磁盤中。binlog是mysql的邏輯日志,并且由Server層進行記錄,使用任何存儲引擎的mysql數據庫都會記錄binlog日志。可通過解析binlog文件來查看數據庫的操作歷史記錄

業內比較成熟的有中間件Canal,我司也用的這個,Canal會模擬MySQL主從復制的交互協議,把自己偽裝成一個 MySQL 的從節點,向MySQL主節點發送dump請求,MySQL收到請求后,就會開始推送Binlog給Canal,Canal解析Binlog字節流,解析出其中有關數據庫中數據更新的日志,解析日志并執行對應數據的刪除緩存操作,然后再引入MQ,通過消息隊列的ACK機制,來確保這條消息的執行成功

圖片圖片

關注我,小牛呼嚕嚕,我再說幾句:

希望大家通過這些方案的學習,能夠領悟為什么只能滿足AP?

為什么緩存的數據一致性問題是無法避免的挑戰?

引入緩存后,我們該如何監控起來呢?進一步分析過期時間是否合適,緩存的命中率

或者是否必需引入緩存?不引入緩存可就沒有緩存的數據一致性,這些都需要數據分析作為支撐

或者引入緩存如何進一步優化,緩存的key如何花式設置,緩存預熱有講究,還有團隊如何規范使用緩存等等,有太多可以深究

分享到:
標簽:緩存
用戶無頭像

網友整理

注冊時間:

網站:5 個   小程序:0 個  文章:12 篇

  • 52003

    網站

  • 12

    小程序

  • 1047590

    文章

  • 762

    會員

趕快注冊賬號,推廣您的網站吧!
最新入駐小程序

數獨大挑戰2018-06-03

數獨一種數學游戲,玩家需要根據9

答題星2018-06-03

您可以通過答題星輕松地創建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學四六

運動步數有氧達人2018-06-03

記錄運動步數,積累氧氣值。還可偷

每日養生app2018-06-03

每日養生,天天健康

體育訓練成績評定2018-06-03

通用課目體育訓練成績評定