1. 程式人生 > >nginx+redis+tomcat三級快取架構講解

nginx+redis+tomcat三級快取架構講解

對於一個大型的資料快取系統,會部署多層快取服務來達到高併發、高可用的系統需求

  • nginx層
    對於傳統的快取系統,請求到達nginx,然後分發到對應的服務系統,然後查詢redis中是否有資料,然後將結果返回,這裡會花費很大的網路開銷。nginx可以支援熱資料的快取,對這些資料直接快取在nginx記憶體中,請求到來直接返回,不需要去傳送網路請求。但是nginx記憶體的大小一般會很小,所以適合快取熱資料

  • redis cluster層
    這一層的資料是最完整的,大部分的離散請求都會訪問到這一層。因此一定要做到高可用,可水平伸縮的模式

  • tomcat層
    tomcat中的堆也可以快取一定的資料,但是容量也不會太大。主要是用來防止redis層大面積崩潰之後,大量資料請求直接到DB層

對於資料不同的實時性要求,我們可以採取不同的方式來修改快取

對於實時性較高的資料(例如庫存資料):

對於這種實時性很高的資料,當有修改的時候,一般是修改DB然後同時去修改redis快取的雙寫模式。但是這裡會設計到資料不一致的問題。

  • 最初級不一致問題及解決方案
    對於先修改資料庫,再刪除快取,如果快取刪除失敗,那麼回導致資料庫中是最新資料,快取中是舊資料,出現不一致的情況。
    解決方法:先刪除快取,再修改資料庫
  • 比較複雜的資料不一致問題
    對於高併發的請求,讀寫操作可能會產生不一致的問題,在寫的過程中,另一個讀操作重新讀取到來舊資料。當然對於併發不高的系統,這種情況也基本不會遇到。
    解決方案如下:

資料庫、快取更新與讀取操作進行非同步序列化

將需要更新的資料,傳送到jvm內部的佇列中,一般會維護多個記憶體佇列,對資料的唯一標識hash然後對記憶體佇列數量取模,就可以保證同樣的資料操作一定會在同一個佇列中去。
每個佇列有一個工作執行緒,每個工作執行緒序列拿到對應的操作,然後逐條執行。因此一個數據的變更,會先刪除快取,然後再去更新資料庫,當還沒有更新完成的時候,另一個讀請求過來來,讀到空的快取,那麼可以將這個快取更新的請求傳送到佇列中,從而會同步等待快取更新完成。需要注意的一點,對於快取為空,多個讀請求過來,將多個更新快取的請求傳送到佇列是沒有必要的,因此需要做過濾,然後可以將這些讀請求hang住一個時間段,輪詢去查詢快取,當前面佇列中的更新快取請求執行完成後,就可以拿到快取資料直接返回,否則,可以讓服務直接取查詢DB來拿資料。

在高併發下,需要注意:

  • 對於快取中沒有該資料,也可能是資料庫本身就沒有。因此需要判斷佇列中是否有更新該資料的操作,如果沒有則讀請求沒有必要hang住,直接返回空即可
  • 需要線上模擬壓力測試,當記憶體佇列積壓過多的更新資料操作,會對最後的讀操作產生於一定的延時,此時如果需要優化就需要加機器,分配到更平均的佇列數量中

基於zookeeper分散式鎖解決多服務快取重建併發衝突:

對於多個快取服務,可能存在這種情況,nginx請求到達發現都沒有相應快取資料,會發送請求給快取服務到資料來源拉取資料並寫入相應tomcat/redis,同時kafka生產了訊息的變更記錄也需要去拉取資料並存入tomcat/redis。兩者拉取資料來源與快取重建可能會出現併發衝突導致tomcat/redis中的最終資料並不是最新。

對於實時性不高的資料

對於實時性不高的資料,如果發生了變更,幾分鐘之後才更新到頁面上,我們採取非同步更新快取的策略。快取資料生產服務,監聽一個訊息佇列,然後資料來源服務(商品資訊管理服務)發生來變更之後,就將資料變更的訊息推送到訊息佇列(topic),快取資料生產服務可以去消費這些變更的資訊,然後根據訊息的提示提取一些引數,然後呼叫對應的資料來源服務介面拉取資料,一般是從mysql拉取,然後將資料存放到本地堆快取和redis快取

對於nginx層的快取

一般來說,預設會部署多個nginx,在裡面都會放一些快取,此時快取的命中率會非常底,因為對同樣資料的請求可能被路由到多個nginx從而未能找到相應快取而多次對redis發起請求。因此,可以採用分發層+應用層雙層ngin來提升快取命中率。

  • 分發層nginx:負責流量分發的邏輯和策略,根據自己定義的一些規則,比如根據productId進行hash,然後對後端nginx數量取模將某一個商品的訪問請求固定路由到一個nginx後端伺服器上去,保證了nginx只會對該商品資料從redis獲取一次。之後的同樣商品請求都直接走nginx快取。
  • 後端nginx: 稱之為應用伺服器