分散式緩存架構設計

来源:https://www.cnblogs.com/mylanqiu/archive/2018/10/08/9753259.html
-Advertisement-
Play Games

零、 題記 在高併發場景下,需要通過緩存來減少資料庫的壓力,使得大量的訪問進來能夠命中緩存,只有少量的需要到資料庫層。由於緩存基於記憶體,可支持的併發量遠遠大於基於硬碟的資料庫。所以對於高併發設計,緩存的設計是必不可少的一環。一、為什麼要使用緩存 為什麼要使用緩存呢?源於人類的一個夢想,就是多快好省的 ...


零、 題記

在高併發場景下,需要通過緩存來減少資料庫的壓力,使得大量的訪問進來能夠命中緩存,只有少量的需要到資料庫層。由於緩存基於記憶體,可支持的併發量遠遠大於基於硬碟的資料庫。所以對於高併發設計,緩存的設計是必不可少的一環。

一、為什麼要使用緩存

為什麼要使用緩存呢?源於人類的一個夢想,就是多快好省的建設社會主義。多快好省?很多客戶都這麼要求,但是作為具體做技術的你,當然知道,好就不能快,多就沒法省。

可是沒辦法,客戶都這樣要求:
這個能不能便宜一點,你咋這麼貴呀,你看人家都很便宜的。(您好,這種打折的房間比較靠里,是不能面向大海的)
你們的性能怎麼這麼差啊,用你這個系統跑的這麼慢,你看人家廣告中說速度能達到多少多少。(您好,你如果買一個頂配的,我們也是有這種性能的)
你們服務不行啊,你就不能彬彬有禮,穿著整齊,送點水果瓜子啥的?(您好,我們蘭州拉麵館沒有這項服務,可以去對面的俏江南看一下)
這麼貴的菜,一盤就這麼一點點,都吃不飽,就不能上一大盤麽。(您好,對面的蘭州拉麵10塊錢一大碗)
怎麼辦呢?勞動人民還是很有智慧的,就是聚焦核心需求,讓最最核心的部分享用好和快,而非核心的部門就多和省就可以了。
你可以大部分時間住在公司旁邊的出租屋裡面,但是出去度假的一個星期,選一個面朝大海,春暖花開的五星級酒店。
你可以大部分時間都擠地鐵,擠公交,跋涉2個小時從北五環到南五環,但是有急事的時候,你可以打車,想旅游的時候,可以租車。
你可以大部分時間都吃普通的餐館,而朋友來了,就去高級飯店裡面搓一頓。

在電腦世界也是這樣樣子的,如圖所示。

越是快的設備,存儲量越小,越貴,而越是慢的設備,存儲量越大,越便宜。

對於一家電商來講,我們既希望存儲越來越多的數據,因為數據將來就是資產,就是財富,只有有了數據,我們才知道用戶需要什麼,同時又希望當我想訪問這些數據的時候,能夠快速的得到,雙十一拼的就是速度和用戶體驗,要讓用戶有流暢的感覺。

所以我們要講大量的數據都保存下來,放在便宜的存儲裡面,同時將經常訪問的,放在貴的,小的存儲裡面,當然貴的快的往往比較資源有限,因而不能長時間被某些數據長期霸占,所以要大家輪著用,所以叫緩存,也就是暫時存著。

二、都有哪些類型的緩存

當一個應用剛開始的時候,架構比較簡單,往往就是一個Tomcat,後面跟著一個資料庫。

簡單的應用,併發量不大的時候,當然沒有問題。

然而資料庫相當於我們應用的中軍大帳,是我們整個架構中最最關鍵的一部分,也是最不能掛,也最不能會被攻破的一部分,因而所有對資料庫的訪問都需要一道屏障來進行保護,常用的就是緩存。
我們以Tomcat為分界線,之外我們稱為接入層,接入層當然應該有緩存,還有CDN。
Tomcat之後,我們稱為應用層,應用層也應該有緩存,這是我們這一節討論的重點。
最簡單的方式就是Tomcat裡面有一層緩存,常稱為本地緩存LocalCache。

這類的緩存常見的有Ehcache和Guava Cache,由於這類緩存在Tomcat本地,因而訪問速度是非常快的。

但是本地緩存有個比較大的缺點,就是緩存是放在JVM裡面的,會面臨Full GC的問題,一旦出現了FullGC,就會對應用的性能和相應時間產生影響,當然也可以嘗試jemalloc的分配方式。

還有一種方式,就是在Tomcat和Mysql中間加了一層Cache,我們常稱為分散式緩存。

分散式緩存常見的有Memcached和Redis,兩者各有優缺點。

Memcached適合做簡單的key-value存儲,記憶體使用率比較高,而且由於是多核處理,對於比較大的數據,性能較好。

但是缺點也比較明顯,Memcached嚴格來講沒有集群機制,橫向擴展完全靠客戶端來實現。另外Memcached無法持久化,一旦掛了數據就都丟失了,如果想實現高可用,也是需要客戶端進行雙寫才可以。

所以可以看出Memcached真的是設計出來,簡簡單單為了做一個緩存的。

Redis的數據結構就豐富的多了,單線程的處理所有的請求,對於比較大的數據,性能稍微差一點。

Redis提供持久化的功能,包括RDB的全量持久化,或者AOF的增量持久化,從而使得Redis掛了,數據是有機會恢復的。

Redis提供成熟的主備同步,故障切換的功能,從而保證了高可用性。

所以很多地方管Redis稱為記憶體資料庫,因為他的一些特性已經有了資料庫的影子。

這也是很多人願意用Redis的原因,集合了緩存和資料庫的優勢,但是往往會濫用這些優勢,從而忽略了架構層面的設計,使得Redis集群有很大的風險。

很多情況下,會將Redis當做資料庫使用,開啟持久化和主備同步機制,以為就可以高枕無憂了。

然而Redis的持久化機制,全量持久化則往往需要額外較大的記憶體,而在高併發場景下,記憶體本來就很緊張,如果造成swap,就會影響性能。增量持久化也涉及到寫磁碟和fsync,也是會拖慢處理的速度,在平時還好,如果高併發場景下,仍然會影響吞吐量。

所以在架構設計角度,緩存就是緩存,要意識到數據會隨時丟失的,要意識到緩存的存著的目的是攔截到資料庫的請求。如果為了保證緩存的數據不丟失,從而影響了緩存的吞吐量,甚至穩定性,讓緩存響應不過來,甚至掛掉,所有的請求擊穿到資料庫,就是更加嚴重的事情了。

如果非常需要進行持久化,可以考慮使用levelDB此類的,對於隨機寫入性能較好的key-value持久化存儲,這樣只有部分的確需要持久化的數據,才進行持久化,而非無論什麼數據,通通往Redis裡面扔,同時統一開啟了持久化。

三、基於緩存的架構設計要點

所以基於緩存的設計:

1、多層次
這樣某一層的緩存掛了,還有另一層可以撐著,等待緩存的修複,例如分散式緩存因為某種原因掛了,因為持久化的原因,同步機制的原因,記憶體過大的原因等,修複需要一段時間,在這段時間內,至少本地緩存可以抗一陣,不至於一下子就擊穿資料庫。而且對於特別特別熱的數據,熱到導致集中式的緩存處理不過來,網卡也被打滿的情況,由於本地緩存不需要遠程調用,也是分佈在應用層的,可以緩解這種問題。

2、分場景
到底要解決什麼問題,可以選擇不同的緩存。是要存儲大的無格式的數據,還是要存儲小的有格式的數據,還是要存儲一定需要持久化的數據。具體的場景下一節詳細談。

3、要分片
使得每一個緩存實例都不大,但是實例數目比較多,這樣一方面可以實現負載均衡,防止單個實例稱為瓶頸或者熱點,另一方面如果一個實例掛了,影響面會小很多,高可用性大大增強。分片的機制可以在客戶端實現,可以使用中間件實現,也可以使用Redis的Cluster的方式,分片的演算法往往都是哈希取模,或者一致性哈希。

四、緩存的使用場景

當你的應用扛不住,知道要使用緩存了,應該怎麼做呢?

場景1:和資料庫中的數據結構保持一致,原樣緩存
這種場景是最常見的場景,也是很多架構使用緩存的適合,最先涉及到的場景。

基本就是資料庫裡面啥樣,我緩存也啥樣,資料庫裡面有商品信息,緩存裡面也放商品信息,唯一不同的是,資料庫裡面是全量的商品信息,緩存裡面是最熱的商品信息。

每當應用要查詢商品信息的時候,先查緩存,緩存沒有就查資料庫,查出來的結果放入緩存,從而下次就查到了。

這個是緩存最最經典的更新流程。這種方式簡單,直觀,很多緩存的庫都預設支持這種方式。

 

場景2:列表排序分頁場景的緩存

有時候我們需要獲得一些列表數據,並對這些數據進行排序和分頁。

例如我們想獲取點贊最多的評論,或者最新的評論,然後列出來,一頁一頁的翻下去。

在這種情況下,緩存裡面的數據結構和資料庫裡面完全不一樣。

如果完全使用資料庫進行實現,則按照某種條件將所有的行查詢出來,然後按照某個欄位進行排序,然後進行分頁,一頁一頁的展示。

但是當數據量比較大的時候,這種方式往往成為瓶頸,首先涉及的資料庫行數比較多,而且排序也是個很慢的活,儘管可能有索引,分頁也是翻頁到最後,越是慢。

在緩存裡面,就沒必要每行一個key了,而是可以使用Redis的列表方式進行存儲,當然列表的長短是有限制的,肯定放不下資料庫裡面這麼多,但是大家會發現其實對於所有的列表,用戶往往沒有耐心看個十頁八頁的,例如百度上搜個東西,也是有排序和分頁的,但是你每次都往後翻了嗎,每頁就十條,就算是十頁,或者一百頁,也就一千條數據,如果保持ID的話,完全放的下。

如果已經排好序,放在Redis裡面,那取出列表,翻頁就非常快了。

可以後臺有一個線程,非同步的初始化和刷新緩存,在緩存裡面保存一個時間戳,當有更新的時候,刷新時間戳,非同步任務發現時間戳改變了,就刷新緩存。

場景3:計數緩存
計數對於資料庫來講,是一個非常繁重的工作,需要查詢大量的行,最後得出計數的結論,當數據改變的時候,需要重新刷一遍,非常影響性能。

因此可以有一個計數服務,後端是一個緩存,將計數作為結果放在緩存裡面,當數據有改變的時候,調用計數服務增加或者減少計數,而非通過非同步資料庫count來更新緩存。

計數服務可以使用Redis進行單個計數,或者hash表進行批量計數

場景4:重構維度緩存
有時候資料庫裡面保持的數據的維度是為了寫入方便,而非為了查詢方便的,然而同時查詢過程,也需要處理高併發,因而需要為了查詢方便,將數據重新以另一個維度存儲一遍,或者說將多給資料庫的內容聚合一下,再存儲一遍,從而不用每次查詢的時候都重新聚合,如果還是放在資料庫,比較難維護,放在緩存就好一些。

例如一個商品的所有的帖子和帖子的用戶,以及一個用戶發表過的所有的帖子就是屬於兩個維度。

這需要寫入一個維度的時候,同時非同步通知,更新緩存中的另一個維度。

在這種場景下,數據量相對比較大,因而單純用記憶體緩存memcached或者redis難以支撐,往往會選擇使用levelDB進行存儲,如果levelDB的性能跟不上,可以考慮在levelDB之前,再來一層memcached。

場景5:較大的詳情內容數據緩存
對於評論的詳情,或者帖子的詳細內容,屬於非結構化的,而且內容比較大,因而使用memcached比較好。

五、緩存三大矛盾問題

1、緩存實時性和一致性問題:當有了寫入後咋辦?

雖然使用了緩存,大家心裡都有一個預期,就是實時性和一致性得不到完全的保證,畢竟數據保存了多份,資料庫一份,緩存中一份,當資料庫中因寫入而產生了新的數據,往往緩存是不會和資料庫操作放在一個事務裡面的,如何將新的數據更新到緩存裡面,什麼時候更新到緩存裡面,不同的策略不一樣。

從用戶體驗角度,當然是越實時越好,用戶體驗越流暢,完全從這個角度出發,就應該有了寫入,馬上廢棄緩存,觸發一次資料庫的讀取,從而更新緩存。但是這和第三個問題,高併發就矛盾了,如果所有的都實時從資料庫裡面讀取,高併發場景下,資料庫往往受不了。

2、緩存的穿透問題:當沒有讀到咋辦?

為什麼會出現緩存讀取不到的情況呢?

第一:可能讀取的是冷數據,原來從來沒有訪問過,所以需要到資料庫裡面查詢一下,然後放入緩存,再返回給客戶。

第二:可能數據因為有了寫入,被實時的從緩存中刪除了,就如第一個問題中描述的那樣,為了保證實時性,當資料庫中的數據更新了之後,馬上刪除緩存中的數據,導致這個時候的讀取讀不到,需要到資料庫裡面查詢後,放入緩存,再返回給客戶。

第三:可能是緩存實效了,每個緩存數據都會有實效時間,過了一段時間沒有被訪問,就會失效,這個時候數據就訪問不到了,需要訪問資料庫後,再放入緩存。

第四:數據被換出,由於緩存記憶體是有限的,當使用快滿了的時候,就會使用類似LRU策略,將不經常使用的數據換出,所以也要訪問資料庫。

第五:後端確實也沒有,應用訪問緩存沒有,於是查詢資料庫,結果資料庫裡面也沒有,只好返回客戶為空,但是尷尬的是,每次出現這種情況的時候,都會面臨著一次資料庫的訪問,純屬浪費資源,常用的方法是,講這個key對應的結果為空的事實也進行緩存,這樣緩存可以命中,但是命中後告訴客戶端沒有,減少了資料庫的壓力。

無論哪種原因導致的讀取緩存讀不到的情況,該怎麼辦?是個策略問題。

一種是同步訪問資料庫後,放入緩存,再返回給客戶,這樣實時性最好,但是給資料庫的壓力也最大。

另一種方式就是非同步的訪問資料庫,暫且返回客戶一個fallback值,然後同時觸發一個非同步更新,這樣下次就有了,這樣資料庫壓力小很多,但是用戶就訪問不到實時的數據了。

3、緩存對資料庫高併發訪問:都來訪問資料庫咋辦?
我們本來使用緩存,是來攔截直接訪問資料庫請求的,從而保證資料庫大本營永遠處於健康的狀態。但是如果一遇到不命中,就訪問資料庫的話,平時沒有什麼問題,但是大促情況下,資料庫是受不了的。

一種情況是多個客戶端,併發狀態下,都不命中了,於是併發的都來訪問資料庫,其實只需要訪問一次就好,這種情況可以通過加鎖,只有一個到後端來實現。

另外就是即便採取了上述的策略,依然併發量非常大,後端的資料庫依然受不了,則需要通過降低實時性,將緩存攔在資料庫前面,暫且撐住,來解決。

六、解決緩存三大矛盾的刷新策略

1、實時策略
所謂的實時策略,是平時緩存使用的最常用的策略,也是保持實時性最好的策略。

讀取的過程,應用程式先從cache取數據,沒有得到,則從資料庫中取數據,成功後,放到緩存中。如果命中,應用程式從cache中取數據,取到後返回。

寫入的過程,把數據存到資料庫中,成功後,再讓緩存失效,失效後下次讀取的時候,會被寫入緩存。那為什麼不直接寫緩存呢?因為如果兩個線程同時更新資料庫,一個將資料庫改為10,一個將資料庫改為20,資料庫有自己的事務機制,可以保證如果20是後提交的,資料庫裡面改為20,但是回過頭來寫入緩存的時候就沒有事務了,如果改為20的線程先更新緩存,改為10的線程後更新緩存,於是就會長時間出現緩存中是10,但是資料庫中是20的現象。

這種方式實時性好,用戶體驗好,是預設應該使用的策略。

2、非同步策略
所謂非同步策略,就是當讀取的時候讀不到的時候,不直接訪問資料庫,而是返回一個fallback數據,然後往消息隊列裡面放入一個數據載入的事件,在背後有一個任務,收到事件後,會非同步的讀取資料庫,由於有隊列的作用,可以實現消峰,緩衝對資料庫的訪問,甚至可以將多個隊列中的任務合併請求,合併更新緩存,提高了效率。

當更新的時候,非同步策略總是先更新資料庫和緩存中的一個,然後非同步的更新另一個。

一是先更新資料庫,然後非同步更新緩存。當資料庫更新後,同樣生成一個非同步消息,放入消息隊列中,等待背後的任務通過消息進行緩存更新,同樣可以實現消峰和任務合併。缺點就是實時性比較差,估計要過一段時間才能看到更新,好處是數據持久性可以得到保證。

一是先更新緩存,然後非同步更新資料庫。這種方式讀取和寫入都用緩存,將緩存完全擋在了資料庫的前面,把緩存當成了資料庫在用。所以一般會使用有持久化機制和主備的redis,但是仍然不能保證緩存不丟數據,所以這種情況適用於併發量大,但是數據沒有那麼關鍵的情況,好處是實時性好。

在實時策略扛不住大促的時候,可以根據場景,切換到上面的兩種模式的一個,算是降級策略。

3、定時策略
如果併發量實在太大,數據量也大的情況,非同步都難以滿足,可以降級為定時刷新的策略,這種情況下,應用只訪問緩存,不訪問資料庫,更新頻率也不高,而且用戶要求也不高,例如詳情,評論等。

這種情況下,由於數據量比較大,建議將一整塊數據拆分成幾部分進行緩存,而且區分更新頻繁的和不頻繁的,這樣不用每次更新的時候,所有的都更新,只更新一部分。並且緩存的時候,可以進行數據的預整合,因為實時性不高,讀取預整合的數據更快。

本文轉載自:http://www.dalbll.com/Group/Topic/ArchitecturedDesign/5205


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 網上很多關於驗證小數的正則表達式,但是很多都不是百分百正確,所以我結合一些前輩的經驗,自己寫了一個。 驗證非0開頭的無限位整數和小數。整數支持無限位,小數點前支持無限位,小數點後最多保留兩位。 js代碼如下: var reg = /^(([^0][0-9]+|0)\.([0-9]{1,2})$)|^ ...
  • 近幾年,微服務架構在後端技術社區大紅大紫,它被認為是IT軟體架構的未來技術方向.我們如何借鑒後端微服務的思想來構建一個現代化前端應用? 在這裡我提供一個可以在產品中真正可以落地的前端微服務解決方案. 微服務化後端前後端對比 後端微服務化的優勢: 1. 複雜度可控: 體積小、複雜度低,每個微服務可由一 ...
  • 在Bootstrap fileinput中移除預覽文件時可以通過配置initialPreviewConfig: [ { url:'deletefile',key:fileid } ] 來同步刪除伺服器上的文件和記錄。但新上傳的文件則需要其他方式來同步刪除伺服器記錄。 在配置中遇到的一些問題,記錄一下 ...
  • 數據流轉 先上一張圖看清 Westore 怎麼解決小程式數據難以管理和維護的問題: 非純組件的話,可以直接省去 triggerEvent 的過程,直接修改 store.data 並且 update,形成縮減版單向數據流。 "Github: https://github.com/dntzhang/we ...
  • 想在黑暗中看清周圍,不可避免地要用到夜視儀。那麼如果是想在黑暗中拍照,又沒有閃光燈,如何才能排到清晰的照片?在CVPR 2018上,英特爾實驗室的Vladlen Koltun和陳啟峰帶領的團隊提出了一種在黑暗中快速成像的系統,效果非常贊。 在暗光下的圖像易受到低信噪比和低亮度的影響。短曝光的照片會出 ...
  • 去年的時候寫過dubbo+zipkin調用鏈監控,最近看到zipkin2配合brave實現起來會比我之前的實現要簡單很多,因為brave將很多交互的內容都封裝起來了,不需要自己去寫具體的實現,比如如何去構建span,如何去上報數據。 收集器抽象 由於zipkin支持http以及kafka兩種方式上報 ...
  • 這兩年微服務越來越火,使用Consul的人也越來越多,這篇文章將結合Consul的官方文檔和自己的實際經驗,談一下Consul做服務發現的方式,文中儘量不依賴具體的框架和開發語言,從原理上進行說明,希望能夠講清楚幾個問題。 ...
  • 前期我們針對架構準備階段及需求分析這塊我們寫了2篇內容《HRMS(人力資源管理系統)-從單機應用到SaaS應用-架構分析(功能性、非功能性、關鍵約束)-上篇》《HRMS(人力資源管理系統)-從單機應用到SaaS應用-架構分析(功能性、非功能性、關鍵約束)-下篇》內容來展開說明。 本篇... ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...