大型高併發與高可用的三層緩存架構總結

前言:對於高併發架構,毫無疑問緩存是最重要的一環,對於大量的高併發,能夠採用三層緩存架構來實現,nginx+redis+ehcachenode

nginx

對於中間件nginx經常使用來作流量的分發,同時nginx自己也有本身的緩存(容量有限),咱們能夠用來緩存熱點數據,讓用戶的請求直接走緩存並返回,減小流向服務器的流量linux

一.模板引擎nginx

一般咱們能夠配合使用freemaker/velocity等模板引擎來抗住大量的請求redis

小型系統可能直接在服務器端渲染出全部的頁面並放入緩存,以後的相同頁面請求就能夠直接返回,不用去查詢數據源或者作數據邏輯處理 對於頁面很是之多的系統,當模板有改變,上述方法就須要從新渲染全部的頁面模板,毫無疑問是不可取的。所以配合nginx+lua(OpenResty),將模板單獨保存在nginx緩存中,同時對於用來渲染的數據也存在nginx緩存中,可是須要設置一個緩存過時的時間,以儘量保證模板的實時性數據庫

二.雙層nginx來提高緩存命中率apache

對於部署多個nginx而言,若是不加入一些數據的路由策略,那麼可能致使每一個nginx的緩存命中率很低。所以能夠部署雙層nginx後端

分發層nginx負責流量分發的邏輯和策略,根據本身定義的一些規則,好比根據productId進行hash,而後對後端nginx數量取模將某一個商品的訪問請求固定路由到一個nginx後端服務器上去 後端nginx用來緩存一些熱點數據到本身的緩存區(分發層只能配置1個嗎)緩存

redis

用戶的請求,在nginx沒有緩存相應的數據,那麼會進入到redis緩存中,redis能夠作到全量數據的緩存,經過水平擴展可以提高併發、高可用的能力tomcat

一.持久化機制: 將redis內存中的數據持久化到磁盤中,而後能夠按期將磁盤文件上傳至S3(AWS)或者ODPS(阿里雲)等一些雲存儲服務上去。服務器

若是同時使用RDB和AOF兩種持久化機制,那麼在redis重啓的時候,會使用AOF來從新構建數據,由於AOF中的數據更加完整,建議將兩種持久化機制都開啓,用AO F來保證數據不丟失,做爲數據恢復的第一選擇;用RDB來做不一樣程度的冷備,在AOF文件都丟失或損壞不可用的時候來快速進行數據的恢復。

實戰踩坑:對於想從RDB恢復數據,同時AOF開關也是打開的,一直沒法正常恢復,由於每次都會優先從AOF獲取數據(若是臨時關閉AOF,就能夠正常恢復)。此時首先中止redis,而後關閉AOF,拷貝RDB到相應目錄,啓動redis以後熱修改配置參數redis config set appendonly yes,此時會自動生成一個當前內存數據的AOF文件,而後再次中止redis,打開AOF配置,再次啓動數據就正常啓動

1.RDB

對redis中的數據執行週期性的持久化,每一刻持久化的都是全量數據的一個快照。對redis性能影響較小,基於RDB可以快速異常恢復

2.AOF

以append-only的模式寫入一個日誌文件中,在redis重啓的時候能夠經過回放AOF日誌中的寫入指令來從新構建整個數據集。(實際上每次寫的日誌數據會先到linux os cache,而後redis每隔一秒調用操做系統fsync將os cache中的數據寫入磁盤)。對redis有必定的性能影響,可以儘可能保證數據的完整性。redis經過rewrite機制來保障AOF文件不會太龐大,基於當前內存數據並能夠作適當的指令重構。

二.redis集羣

1.replication

一主多從架構,主節點負責寫,而且將數據同步到其餘salve節點(異步執行),從節點負責讀,主要就是用來作讀寫分離的橫向擴容架構。這種架構的master節點數據必定要作持久化,不然,當master宕機重啓以後內存數據清空,那麼就會將空數據複製到slave,致使全部數據消失

2.sentinal哨兵

哨兵是redis集羣架構中很重要的一個組件,負責監控redis master和slave進程是否正常工做,當某個redis實例故障時,可以發送消息報警通知給管理員,當master node宕機可以自動轉移到slave node上,若是故障轉移發生來,會通知client客戶端新的master地址。sentinal至少須要3個實例來保證本身的健壯性,而且可以更好地進行quorum投票以達到majority來執行故障轉移。 前兩種架構方式最大的特色是,每一個節點的數據是相同的,沒法存取海量的數據。所以哨兵集羣的方式使用與數據量不大的狀況

3.redis cluster

redis cluster支撐多master node,每一個master node能夠掛載多個slave node,若是mastre掛掉會自動將對應的某個slave切換成master。須要注意的是redis cluster架構下slave節點主要是用來作高可用、故障主備切換的,若是必定須要slave可以提供讀的能力,修改配置也能夠實現(同時也須要修改jedis源碼來支持該狀況下的讀寫分離操做)。redis cluster架構下,master就是能夠任意擴展的,直接橫向擴展master便可提升讀寫吞吐量。slave節點可以自動遷移(讓master節點儘可能平均擁有slave節點),對整個架構過載冗餘的slave就能夠保障系統更高的可用性。

ehcache

tomcat jvm堆內存緩存,主要是抗redis出現大規模災難。若是redis出現了大規模的宕機,致使nginx大量流量直接涌入數據生產服務,那麼最後的tomcat堆內存緩存也能夠處理部分請求,避免全部請求都直接流向DB

針對上面的技術我特地整理了一下,有不少技術不是靠幾句話能講清楚,因此乾脆找朋友錄製了一些視頻,不少問題其實答案很簡單,可是背後的思考和邏輯不簡單,要作到知其然還要知其因此然。若是想學習Java工程化、高性能及分佈式、深刻淺出。微服務、Spring,MyBatis,Netty源碼分析的朋友能夠加個人Java進階羣:694549689,羣裏有阿里大牛直播講解技術,以及Java大型互聯網技術的視頻免費分享給你們。

緩存數據更新策略

對時效性要求高的緩存數據,當發生變動的時候,直接採起數據庫和redis緩存雙寫的方案,讓緩存時效性最高。

對時效性不高的數據,當發生變動以後,採起MQ異步通知的方式,經過數據生產服務來監聽MQ消息,而後異步去拉取服務的數據更新tomcat jvm緩存和redis緩存,對於nginx本地緩存過時以後就能夠從redis中拉取新的數據並更新到nginx本地。

經典的緩存+數據庫讀寫的模式,cache aside pattern

1.讀的時候,先讀緩存,緩存沒有的話,那麼就讀數據庫,而後取出數據後放入緩存,同時返回響應

2.更新的時候,先刪除緩存,而後再更新數據庫之因此更新的時候只是刪除緩存,由於對於一些複雜有邏輯的緩存數據,每次數據變動都更新一次緩存會形成額外的負擔,只是刪除緩存,讓該數據下一次被使用的時候再去執行讀的操做來從新緩存,這裏採用的是懶加載的策略。舉個例子,一個緩存涉及的表的字段,在1分鐘內就修改了20次,或者是100次,那麼緩存跟新20次,100次;可是這個緩存在1分鐘內就被讀取了1次,所以每次更新緩存就會有大量的冷數據,對於緩存符合28黃金法則,20%的數據,佔用了80%的訪問量

數據庫和redis緩存雙寫不一致的問題

1.最初級的緩存不一致問題以及解決方案

問題:若是先修改數據庫再刪除緩存,那麼當緩存刪除失敗來,那麼會致使數據庫中是最新數據,緩存中依舊是舊數據,形成數據不一致。

解決方案:能夠先刪除緩存,再修改數據庫,若是刪除緩存成功可是數據庫修改失敗,那麼數據庫中是舊數據,緩存是空不會出現不一致

2.比較複雜的數據不一致問題分析

問題:對於數據發生來變動,先刪除緩存,而後去修改數據庫,此時數據庫中的數據尚未修改爲功,併發的讀請求到來去讀緩存發現是空,進而去數據庫查詢到此時的舊數據放到緩存中,而後以前對數據庫數據的修改爲功來,就會形成數據不一致

解決方案:將數據庫與緩存更新與讀取操做進行異步串行化。當更新數據的時候,根據數據的惟一標識,將更新數據操做路由到一個jvm內部的隊列中,一個隊列對應一個工做線程,線程串行拿到隊列中的操做一條一條地執行。當執行隊列中的更新數據操做,刪除緩存,而後去更新數據庫,此時尚未完成更新的時候過來一個讀請求,讀到了空的緩存那麼能夠先將緩存更新的請求發送至路由以後的隊列中,此時會在隊列積壓,而後同步等待緩存更新完成,一個隊列中多個相同數據緩存更新請求串在一塊兒是沒有意義的,所以能夠作過濾處理。等待前面的更新數據操做完成數據庫操做以後,纔會去執行下一個緩存更新的操做,此時會從數據庫中讀取最新的數據,而後寫入緩存中,若是請求還在等待時間範圍內,不斷輪詢發現能夠取到緩存中值就能夠直接返回(此時可能會有對這個緩存數據的多個請求正在這樣處理);若是請求等待事件超過必定時長,那麼這一次的請求直接讀取數據庫中的舊值

對於這種處理方式須要注意一些問題:

1.讀請求長時阻塞:因爲讀請求進行來很是輕度的異步化,因此對超時的問題須要格外注意,超過超時時間會直接查詢DB,處理很差會對DB形成壓力,所以須要測試系統高峯期QPS來調整機器數以及對應機器上的隊列數最終決定合理的請求等待超時時間

2.多實例部署的請求路由:可能這個服務會部署多個實例,那麼必須保證對應的請求都經過nginx服務器路由到相同的服務實例上

3.熱點數據的路由導師請求的傾斜:由於只有在商品數據更新的時候纔會清空緩存,而後纔會致使讀寫併發,因此更新頻率不是過高的話,這個問題的影響並非特別大,可是的確可能某些機器的負載會高一些

分佈式緩存重建併發衝突解決方案

對於緩存生產服務,可能部署在多臺機器,當redis和ehcache對應的緩存數據都過時不存在時,此時可能nginx過來的請求和kafka監聽的請求同時到達,致使二者最終都去拉取數據而且存入redis中,所以可能產生併發衝突的問題,能夠採用redis或者zookeeper相似的分佈式鎖來解決,讓請求的被動緩存重建與監聽主動的緩存重建操做避免併發的衝突,當存入緩存的時候經過對比時間字段廢棄掉舊的數據,保存最新的數據到緩存

緩存冷啓動以及緩存預熱解決方案

當系統第一次啓動,大量請求涌入,此時的緩存爲空,可能會致使DB崩潰,進而讓系統不可用,一樣當redis全部緩存數據異常丟失,也會致使該問題。所以,能夠提早放入數據到redis避免上述冷啓動的問題,固然也不多是全量數據,能夠根據相似於當天的具體訪問狀況,實時統計出訪問頻率較高的熱數據,這裏熱數據也比較多,須要多個服務並行的分佈式去讀寫到redis中(因此要基於zk分佈式鎖)

經過nginx+lua將訪問流量上報至kafka中,storm從kafka中消費數據,實時統計處每一個商品的訪問次數,訪問次數基於LRU(apache commons collections LRUMap)內存數據結構的存儲方案,使用LRUMap去存放是由於內存中的性能高,沒有外部依賴,每一個storm task啓動的時候基於zk分佈式鎖將本身的id寫入zk同一個節點中,每一個storm task負責完成本身這裏的熱數據的統計,每隔一段時間就遍歷一下這個map,而後維護一個前1000的數據list,而後去更新這個list,最後開啓一個後臺線程,每隔一段時間好比一分鐘都將排名的前1000的熱數據list同步到zk中去,存儲到這個storm task對應的一個znode中去

部署多個實例的服務,每次啓動的時候就會去拿到上述維護的storm task id列表的節點數據,而後根據taskid,一個一個去嘗試獲取taskid對應的znode的zk分佈式鎖,若是可以獲取到分佈式鎖,再去獲取taskid status的鎖進而查詢預熱狀態,若是沒有被預熱過,那麼就將這個taskid對應的熱數據list取出來,從而從DB中查詢出來寫入緩存中,若是taskid分佈式鎖獲取失敗,快速拋錯進行下一次循環獲取下一個taskid的分佈式鎖便可,此時就是多個服務實例基於zk分佈式鎖作協調並行的進行緩存的預熱

緩存熱點致使系統不可用解決方案

對於瞬間大量的相同數據的請求涌入,可能致使該數據通過hash策略以後對應的應用層nginx被壓垮,若是請求繼續就會影響至其餘的nginx,最終致使全部nginx出現異常整個系統變得不可用。

基於nginx+lua+storm的熱點緩存的流量分發策略自動降級來解決上述問題的出現,能夠設定訪問次數大於後95%平均值n倍的數據爲熱點,在storm中直接發送http請求到流量分發的nginx上去,使其存入本地緩存,而後storm還會將熱點對應的完整緩存數據沒發送到全部的應用nginx服務器上去,並直接存放到本地緩存。對於流量分發nginx,訪問對應的數據,若是發現是熱點標識就當即作流量分發策略的降級,對同一個數據的訪問從hash到一臺應用層nginx降級成爲分發至全部的應用層nginx。storm須要保存上一次識別出來的熱點List,並同當前計算出來的熱點list作對比,若是已經不是熱點數據,則發送對應的http請求至流量分發nginx中來取消對應數據的熱點標識

緩存雪崩解決方案

redis集羣完全崩潰,緩存服務大量對redis的請求等待,佔用資源,隨後緩存服務大量的請求進入源頭服務去查詢DB,使DB壓力過大崩潰,此時對源頭服務的請求也大量等待佔用資源,緩存服務大量的資源所有耗費在訪問redis和源服務無果,最後使自身沒法提供服務,最終會致使整個網站崩潰。

事前的解決方案,搭建一套高可用架構的redis cluster集羣,主從架構、一主多從,一旦主節點宕機,從節點自動跟上,而且最好使用雙機房部署集羣。

事中的解決方案,部署一層ehcache緩存,在redis所有實現狀況下可以抗住部分壓力;對redis cluster的訪問作資源隔離,避免全部資源都等待,對redis cluster的訪問失敗時的狀況去部署對應的熔斷策略,部署redis cluster的降級策略;對源服務訪問的限流以及資源隔離

過後的解決方案:redis數據作了備份能夠直接恢復,重啓redis便可;redis數據完全失敗來或者數據過舊,能夠快速緩存預熱,而後讓redis從新啓動。而後因爲資源隔離的half-open策略發現redis已經可以正常訪問,那麼全部的請求將自動恢復

緩存穿透解決方案

對於在多級緩存中都沒有對應的數據,而且DB也沒有查詢到數據,此時大量的請求都會直接到達DB,致使DB承載高併發的問題。解決緩存穿透的問題能夠對DB也沒有的數據返回一個空標識的數據,進而保存到各級緩存中,由於有對數據修改的異步監聽,因此當數據有更新,新的數據會被更新到緩存匯中。

nginx緩存失效致使redis壓力倍增

能夠在nginx本地,設置緩存數據的時候隨機緩存的有效期,避免同一時刻緩存都失效而大量請求直接進入redis

這個過程值得咱們去深刻學習和思考。

相關文章
相關標籤/搜索