原文連接:微服務化之緩存的設計(做者:劉超)redis
在高併發場景下,須要經過緩存來減小數據庫的壓力,使得大量的訪問進來可以命中緩存,只有少許的須要到數據庫層。因爲緩存基於內存,可支持的併發量遠遠大於基於硬盤的數據庫。因此對於高併發設計,緩存的設計時必不可少的一環。算法
爲何要使用緩存呢?源於人類的一個夢想,就是多快好省的建設社會主義。
多快好省?不少客戶都這麼要求,可是做爲具體作技術的你,固然知道,好就不能快,多就無法省。
但是沒辦法,客戶都這樣要求:sql
怎麼辦呢?勞動人民仍是頗有智慧的,就是聚焦核心需求,讓最最核心的部分享用好和快,而非核心的部門就多和省就能夠了。
你能夠大部分時間住在公司旁邊的出租屋裏面,可是出去度假的一個星期,選一個面朝大海,春暖花開的五星級酒店。
你能夠大部分時間都擠地鐵,擠公交,跋涉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裏面扔,同時統一開啓了持久化。
因此基於緩存的設計:
這樣某一層的緩存掛了,還有另外一層能夠撐着,等待緩存的修復,例如分佈式緩存由於某種緣由掛了,由於持久化的緣由,同步機制的緣由,內存過大的緣由等,修復須要一段時間,在這段時間內,至少本地緩存能夠抗一陣,不至於一會兒就擊穿數據庫。並且對於特別特別熱的數據,熱到致使集中式的緩存處理不過來,網卡也被打滿的狀況,因爲本地緩存不須要遠程調用,也是分佈在應用層的,能夠緩解這種問題。
到底要解決什麼問題,能夠選擇不一樣的緩存。是要存儲大的無格式的數據,仍是要存儲小的有格式的數據,仍是要存儲必定須要持久化的數據。具體的場景下一節詳細談。
使得每個緩存實例都不大,可是實例數目比較多,這樣一方面能夠實現負載均衡,防止單個實例稱爲瓶頸或者熱點,另外一方面若是一個實例掛了,影響面會小不少,高可用性大大加強。分片的機制能夠在客戶端實現,可使用中間件實現,也可使用Redis的Cluster的方式,分片的算法每每都是哈希取模,或者一致性哈希。
當你的應用扛不住,知道要使用緩存了,應該怎麼作呢?
這種場景是最多見的場景,也是不少架構使用緩存的適合,最早涉及到的場景。
基本就是數據庫裏面啥樣,我緩存也啥樣,數據庫裏面有商品信息,緩存裏面也放商品信息,惟一不一樣的是,數據庫裏面是全量的商品信息,緩存裏面是最熱的商品信息。
每當應用要查詢商品信息的時候,先查緩存,緩存沒有就查數據庫,查出來的結果放入緩存,從而下次就查到了。
這個是緩存最最經典的更新流程。這種方式簡單,直觀,不少緩存的庫都默認支持這種方式。
有時候咱們須要得到一些列表數據,並對這些數據進行排序和分頁。
例如咱們想獲取點贊最多的評論,或者最新的評論,而後列出來,一頁一頁的翻下去。
在這種狀況下,緩存裏面的數據結構和數據庫裏面徹底不同。
若是徹底使用數據庫進行實現,則按照某種條件將全部的行查詢出來,而後按照某個字段進行排序,而後進行分頁,一頁一頁的展現。
可是當數據量比較大的時候,這種方式每每成爲瓶頸,首先涉及的數據庫行數比較多,並且排序也是個很慢的活,儘管可能有索引,分頁也是翻頁到最後,越是慢。
在緩存裏面,就不必每行一個key了,而是可使用Redis的列表方式進行存儲,固然列表的長短是有限制的,確定放不下數據庫裏面這麼多,可是你們會發現其實對於全部的列表,用戶每每沒有耐心看個十頁八頁的,例如百度上搜個東西,也是有排序和分頁的,可是你每次都日後翻了嗎,每頁就十條,就算是十頁,或者一百頁,也就一千條數據,若是保持ID的話,徹底放的下。
若是已經排好序,放在Redis裏面,那取出列表,翻頁就很是快了。
能夠後臺有一個線程,異步的初始化和刷新緩存,在緩存裏面保存一個時間戳,當有更新的時候,刷新時間戳,異步任務發現時間戳改變了,就刷新緩存。
計數對於數據庫來說,是一個很是繁重的工做,須要查詢大量的行,最後得出計數的結論,當數據改變的時候,須要從新刷一遍,很是影響性能。
所以能夠有一個計數服務,後端是一個緩存,將計數做爲結果放在緩存裏面,當數據有改變的時候,調用計數服務增長或者減小計數,而非經過異步數據庫count來更新緩存。
計數服務可使用Redis進行單個計數,或者hash表進行批量計數
有時候數據庫裏面保持的數據的維度是爲了寫入方便,而非爲了查詢方便的,然而同時查詢過程,也須要處理高併發,於是須要爲了查詢方便,將數據從新以另外一個維度存儲一遍,或者說將多給數據庫的內容聚合一下,再存儲一遍,從而不用每次查詢的時候都從新聚合,若是仍是放在數據庫,比較難維護,放在緩存就好一些。
例如一個商品的全部的帖子和帖子的用戶,以及一個用戶發表過的全部的帖子就是屬於兩個維度。
這須要寫入一個維度的時候,同時異步通知,更新緩存中的另外一個維度。
在這種場景下,數據量相對比較大,於是單純用內存緩存Memcached或者redis難以支撐,每每會選擇使用levelDB進行存儲,若是levelDB的性能跟不上,能夠考慮在levelDB以前,再來一層Memcached。
對於評論的詳情,或者帖子的詳細內容,屬於非結構化的,並且內容比較大,於是使用Memcached比較好。
雖然使用了緩存,你們內心都有一個預期,就是實時性和一致性得不到徹底的保證,畢竟數據保存了多份,數據庫一份,緩存中一份,當數據庫中因寫入而產生了新的數據,每每緩存是不會和數據庫操做放在一個事務裏面的,如何將新的數據更新到緩存裏面,何時更新到緩存裏面,不一樣的策略不同。
從用戶體驗角度,固然是越實時越好,用戶體驗越流暢,徹底從這個角度出發,就應該有了寫入,立刻廢棄緩存,觸發一次數據庫的讀取,從而更新緩存。可是這和第三個問題,高併發就矛盾了,若是全部的都實時從數據庫裏面讀取,高併發場景下,數據庫每每受不了。
爲何會出現緩存讀取不到的狀況呢?
第一:可能讀取的是冷數據,原來歷來沒有訪問過,因此須要到數據庫裏面查詢一下,而後放入緩存,再返回給客戶。
第二:可能數據由於有了寫入,被實時的從緩存中刪除了,就如第一個問題中描述的那樣,爲了保證明時性,當數據庫中的數據更新了以後,立刻刪除緩存中的數據,致使這個時候的讀取讀不到,須要到數據庫裏面查詢後,放入緩存,再返回給客戶。
第三:多是緩存實效了,每一個緩存數據都會有實效時間,過了一段時間沒有被訪問,就會失效,這個時候數據就訪問不到了,須要訪問數據庫後,再放入緩存。
第四:數據被換出,因爲緩存內存是有限的,當使用快滿了的時候,就會使用相似LRU策略,將不常用的數據換出,因此也要訪問數據庫。
第五:後端確實也沒有,應用訪問緩存沒有,因而查詢數據庫,結果數據庫裏面也沒有,只好返回客戶爲空,可是尷尬的是,每次出現這種狀況的時候,都會面臨着一次數據庫的訪問,純屬浪費資源,經常使用的方法是,講這個key對應的結果爲空的事實也進行緩存,這樣緩存能夠命中,可是命中後告訴客戶端沒有,減小了數據庫的壓力。
不管哪一種緣由致使的讀取緩存讀不到的狀況,該怎麼辦?是個策略問題。
一種是同步訪問數據庫後,放入緩存,再返回給客戶,這樣實時性最好,可是給數據庫的壓力也最大。
另外一種方式就是異步的訪問數據庫,暫且返回客戶一個fallback值,而後同時觸發一個異步更新,這樣下次就有了,這樣數據庫壓力小不少,可是用戶就訪問不到實時的數據了。
咱們原本使用緩存,是來攔截直接訪問數據庫請求的,從而保證數據庫大本營永遠處於健康的狀態。可是若是一遇到不命中,就訪問數據庫的話,平時沒有什麼問題,可是大促狀況下,數據庫是受不了的。
一種狀況是多個客戶端,併發狀態下,都不命中了,因而併發的都來訪問數據庫,其實只須要訪問一次就好,這種狀況能夠經過加鎖,只有一個到後端來實現。
另外就是即使採起了上述的策略,依然併發量很是大,後端的數據庫依然受不了,則須要經過下降實時性,將緩存攔在數據庫前面,暫且撐住,來解決。
所謂的實時策略,是平時緩存使用的最經常使用的策略,也是保持實時性最好的策略。
讀取的過程,應用程序先從cache取數據,沒有獲得,則從數據庫中取數據,成功後,放到緩存中。若是命中,應用程序從cache中取數據,取到後返回。
寫入的過程,把數據存到數據庫中,成功後,再讓緩存失效,失效後下次讀取的時候,會被寫入緩存。那爲何不直接寫緩存呢?由於若是兩個線程同時更新數據庫,一個將數據庫改成10,一個將數據庫改成20,數據庫有本身的事務機制,能夠保證若是20是後提交的,數據庫裏面改成20,可是回過頭來寫入緩存的時候就沒有事務了,若是改成20的線程先更新緩存,改成10的線程後更新緩存,因而就會長時間出現緩存中是10,可是數據庫中是20的現象。
這種方式實時性好,用戶體驗好,是默認應該使用的策略。
所謂異步策略,就是當讀取的時候讀不到的時候,不直接訪問數據庫,而是返回一個fallback數據,而後往消息隊列裏面放入一個數據加載的事件,在背後有一個任務,收到事件後,會異步的讀取數據庫,因爲有隊列的做用,能夠實現消峯,緩衝對數據庫的訪問,甚至能夠將多個隊列中的任務合併請求,合併更新緩存,提升了效率。
當更新的時候,異步策略老是先更新數據庫和緩存中的一個,而後異步的更新另外一個。
一是先更新數據庫,而後異步更新緩存。當數據庫更新後,一樣生成一個異步消息,放入消息隊列中,等待背後的任務經過消息進行緩存更新,一樣能夠實現消峯和任務合併。缺點就是實時性比較差,估計要過一段時間才能看到更新,好處是數據持久性能夠獲得保證。
一是先更新緩存,而後異步更新數據庫。這種方式讀取和寫入都用緩存,將緩存徹底擋在了數據庫的前面,把緩存當成了數據庫在用。因此通常會使用有持久化機制和主備的redis,可是仍然不能保證緩存不丟數據,因此這種狀況適用於併發量大,可是數據沒有那麼關鍵的狀況,好處是實時性好。
在實時策略扛不住大促的時候,能夠根據場景,切換到上面的兩種模式的一個,算是降級策略。
若是併發量實在太大,數據量也大的狀況,異步都難以知足,能夠降級爲定時刷新的策略,這種狀況下,應用只訪問緩存,不訪問數據庫,更新頻率也不高,並且用戶要求也不高,例如詳情,評論等。這種狀況下,因爲數據量比較大,建議將一整塊數據拆分紅幾部分進行緩存,並且區分更新頻繁的和不頻繁的,這樣不用每次更新的時候,全部的都更新,只更新一部分。而且緩存的時候,能夠進行數據的預整合,由於實時性不高,讀取預整合的數據更快。有關緩存就說到這裏,下一節講分佈式事務。