緩存中間件-緩存架構的實現(下)

緩存中間件-緩存架構的實現(下)

前言

緩存架構,說白了就是利用各類手段,來實現緩存,從而下降服務器,乃至數據庫的壓力。html

這裏把以前提出的緩存架構的技術分類放出來:前端

  • 瀏覽器緩存
    • Cookie
    • LocalStorage
    • SessionStorage
  • CDN緩存
  • 負載層緩存
    • Nginx緩存模塊
    • Squid緩存服務器
    • Lua擴展
  • 應用層緩存
    • Etag
    • ThreadLocal
    • Guava
  • 外部緩存
    • Redis
  • 數據庫緩存
    • MySql緩存

前面的《緩存中間件-緩存架構的實現(上)》已經簡單說明了瀏覽器緩存,CDN緩存,負載層緩存。此次將會繼續闡述應用層緩存,外部緩存,數據庫緩存。mysql

應用層緩存

應用層的緩存,每每用戶的請求最終達到了應用服務器,可是未達到數據庫,其涉及應用服務器的具體開發。redis

Etag

之因此將Etag技術放在應用層緩存,是由於用戶的請求一定達到應用層。算法

Etag的意思就是,若是連續兩次請求的請求內容是一致的,那麼兩次響應也應該是一致的。那麼第一次請求的響應,就能夠充當第二次請求的響應。sql

固然實際業務中,也存在兩次請求一致,可是響應不一致(如都是查詢銀行餘額,可是並不同,可能兩次操做中間,工資到帳了)。這就涉及到緩存的數據一致性問題,後面會提到。這裏再也不深刻。數據庫

那麼應用服務器怎麼判斷兩次請求一致呢。它能夠經過兩次請求的hash,進行對比判斷。其中涉及HTTP協議,如304狀態碼,請求協議頭If-None-Match字段,響應協議頭Etag字段。後端

請求流程

服務端已經作好了對應的開發與設置(如Spring的ShallowEtagHeaderFilter())。數組

第一次請求
  1. 客戶端發出請求RequestA
  2. 服務端接收到客戶端的請求RequestA,進行如下處理:
    1. 在應用中,根據請求RequestA計算對應的MD5值
    2. 在返回響應ResponseA的協議頭中的Etag字段設置前面計算出來的MD5值
    3. 返回對應頁面
  3. 客戶端接收到響應ResponseA,在瀏覽器中展現。並在瀏覽器中緩存ResponseA
第二次請求
  1. 客戶端再次發出請求RequestB,而且RequestB與RequestA請求內容相同(如都是請求同一個頁面等)
  2. 服務端接收到客戶端的請求RequestB,進行如下處理:
    1. 根據請求計算的新ETag,並判斷是否與請求RequestB協議頭中的If-None-Match字段對應的值(就是以前ResponseA的ETag字段的值)一致
      1. 若是沒有超限, 在Response中設置協議狀態爲304,向客戶端返回對應ReponseB
  3. 客戶端接收到響應ReponseB,確認其協議狀態爲304,則直接使用以前緩存的響應ResponseA,做爲請求RequestB的返回響應

上述實際上是功能邏輯,若是按照代碼邏輯,其實應該這樣說:瀏覽器

客戶端
  1. 客戶端準備發送請求
  2. 瀏覽器檢測該頁面是否有對應的ETag字段的值
  3. 若是有對應的值,就置入請求的協議頭
  4. 準備穩當後,瀏覽器想服務器發送請求
服務端
  1. 根據請求的協議頭,判斷是否具有Last-Modified/If-None-Match字段
  2. 若是有對應字段,進行如下判斷
    1. 根據請求計算的新ETag,並判斷是否與請求協議頭中的If-None-Match字段對應的值(就是以前ResponseA的ETag字段的值一致
      1. 若是沒有超限,在Response中設置協議狀態爲304,向客戶端返回對應Reponse
  3. 若是上述2中任一條件未知足,則執行如下邏輯:
    1. 在應用中,根據請求RequestA計算對應的MD5值,保存在應用中
    2. 返回對應頁面
    3. 在返回響應ResponseA的協議頭中的Etag字段設置前面計算出來的MD5值

準確地說,這應該是HTTP協議提供的緩存方案,而不只僅只是ETag。由於ETag僅僅與HTTP協議的五大條件請求首部中的If-None-Match與If-Match兩個首部相關。除此以外,還有If-Modified-Since,If-Unmodified-Since,If-Range三個條件請求首部。若是之後有機會專門寫一篇有關HTTP協議的博客。迫切的小夥伴,也能夠翻閱《HTTP權威指南》一書的第七章(尤爲是7.8)。

優點

  • 下降數據庫訪問壓力。若是ETag成功,則直接返回狀態碼304,沒有數據庫操做。
  • 下降應用服務器壓力。若是ETag成功,則直接返回狀態碼304,無需業務操做等,如日誌。
  • 下降帶寬壓力。根據統計代表,通常請求響應模型中,響應的報文大小遠大於請求的保溫大小。那麼若是返回響應的主體爲空,只有304狀態碼等協議頭,則能夠大大下降系統帶寬壓力。

缺點

  • 技術學習投入。若是想要較好利用 ,須要熟悉HTTP協議的緩存設計(包括理念,架構,步驟等)
  • 須要對現有的業務體系,進行必定的調整
  • 數據刷新問題的處理,確保數據的「新鮮度」
  • 應用系統的計算資源佔用。有人提出ETag的MD5計算帶來了對應的應用系統的CPU佔用問題。這個須要說一下:
    • 這取決於具體請求自己是否有比MD5計算更大的CPU佔用問題。
    • 合理的緩存架構設計通常不會有這樣的問題(如靜態資源等CPU佔用少的請求,根本就在前面的瀏覽器,CDN,負載均衡層處理掉了)

實際應用

實際應用部分,主要有兩點須要說起。

  • 因爲If-None-Match的部分缺點,有須要的小夥伴最好引入Last-Modified-Since搭配使用
  • 實際開發方面,Spring提供了ShallowEtagHeaderFilter(),也能夠自行擴展

PS:部分人認爲只須要Last-Modified-Since便可,可是僅使用Last-Modified-Since存在如下問題:

  • 1s週期內的變化,沒法處理(由於Last-Modified-Since記錄的最小時間單位爲秒)
  • 部分數據雖然發生了變化,但其實咱們所須要的內容並無變化(如週期性的重寫等)
  • 部分應用系統的系統時間存在衝突(即集羣內的應用服務器實例的絕對系統時間存在秒級差異。至於集羣的時間統一相關的問題,往後有機會專門寫一篇博客(感受本身立下了無數flag))。

ThreadLocal

ThreadLocal是什麼,我就不在此解釋了。不瞭解的小夥伴,能夠這樣理解:ThreadLocal就是一個類中的靜態Map,其key就是執行線程(調用類實例的線程)的name,而value就是調用位置設置的值。

優點

  • (核心)避免接口定義污染。如應用系統中(同一JVM中)存在A->B->C這樣的操做鏈路。但只有A和C用到了特定參數(如用戶信息),那麼爲了可以調用C,B也必須引入該特定參數(如用戶參數),即便B沒有用到該特定參數。這就形成了接口定義的污染(詳見線程級緩存ThreadLocalCache
  • 數據緩存。因爲ThreadLocal是經過棧封閉的理念實現了線程安全,因此其在一些場景下有着特定的使用。

缺點

  • ThreadLocal緩存設計與學習,及原有系統的改動
  • (核心)因爲可能涉及多線程與調用鏈上多個調用節點,因此設計與問題排查會有較大的難度

實際應用

在我以前接收的IOT項目中,終端系統經過傳感器數據讀取程序與傳感器配置,得到原始數據(包括原始監測值,以及配置表中對應配置(如硬件標識,報警閾值等))。可是原始數據採集後,會進行數據清洗,數據報警評估,數據保存等多個操做。可是其中的數據清洗並不涉及硬件標識,與報警閾值等。因此採用ThreadLocal來保存對應數據(硬件配置),避免方法接口的污染。固然,後來因爲該流程並不都是有先後順序要求,因此添加了事件監聽,進行異步解耦,下降系統複雜度。

GuavaCache

Guava表明着應用級緩存,更準確說是單JVM實例緩存。在原單機系統時,咱們每每並非採用Redis這樣的分佈式緩存(除非是但願利用其數據處理,如GEO處理,集合處理等),而是採用GuavaCache或自定義緩存(自定義緩存的設計,後面會有一篇專門的博客)。

優點

  • 資源佔用小。畢竟只是運行於單機的一種緩存工具
  • 實現了一種簡便的緩存管理工具,知足了大多數單機系統對緩存的需求

劣勢

  • 功能沒有分佈式緩存中間件完善(尤爲是自定義的緩存工具)
  • 若是是採用Guava這樣的第三方緩存工具,須要對工具的必定學習成本
  • 若是是自定義實現(爲了更爲精簡,定製化),每每性能的提升對技術水平有着必定的需求(如SoftReference的利用等)
  • 對原有應用的改變

外部緩存

外部緩存的一個重要表明,就是Redis,Memcache這樣的分佈式緩存中間件。固然外部緩存,你要把文件系統等劃分進來,也不是不行,只要能夠知足對緩存的定義便可。

這裏以Redis爲例。

Redis

Redis做爲當下最爲流行的分佈式緩存中間件,其應用能夠說是很是普遍的,也是我很是喜歡使用的一種分佈式緩存中間件。其是一個開源的,C語言編寫的,基於內存,支持持久化的日誌型,KV型的網絡程序。

優勢

  • 使用簡單。Redis的單機使用不要太簡單。即便是新人,也能夠在很短的時間內上手,並在實際開發中應用(固然,若是項目中已經有了相關配置,並提供了相關Util就更方便了)
  • 性能強悍。即便是單機的Redis,也能夠在一個普通性能的服務器上,提供每秒十萬級的讀寫能力(固然影響的狀況不少,詳見redis的BenchMark
  • 功能強大。Redis提供了GEO的相關操做(計算兩點距離等),集合相關操做(交集,並集等),流相關操做(相似消息隊列)
  • 應用場景多。如Session服務器(分佈式Session的優秀解決方案),計數器(Incr),分佈式鎖等

缺點

  • 須要部署Redis服務器。而且爲了確保可用性,每每須要進行集羣部署
  • 精通較難。
    • 功能方面。功能強大的Redis,其內部實現仍是有很多東西的,包括其持久化機制,內存管理
    • 理論方面。如Redis內存管理方面,涉及LRU,LFU算法,以及其自定義簡化版的實現。又或者其哨兵機制涉及的Raft分佈式選舉算法等
    • 部署方面。單機部署,以及多種集羣部署(生產級部署,能夠看我以前的博客-Redis安裝(單機及各種集羣,阿里雲)

實際應用

在我以前接手過的某綜合系統(涵蓋社交,在線教育,直播等),其Session服務器是經過Redis進行支撐的。經過將<SessionId,Session>的方式,存儲在Redis,而SeesionId會保存在用戶的Cookie中(至於某些小夥伴擔憂的Cookie禁用問題,這就涉及Cookie的知識內容了。Cookie會保存在URL中)

再舉一個例子(Redis的應用場景太多了)。以前負責的IOT項目中,其中控系統的報警模塊有這麼一個需求:同一個終端的同一個傳感器在30min中,只報警一次,避免報警刷屏的現象。而中控系統已經採用了Redis(中控系統是能夠集羣部署,確保可用性,避免性能瓶頸),因此利用Redis的集合特性與expire特性,進行了對應的緩存設計。這個在以後會專門寫一篇博客,進行闡述。

數據庫緩存

這裏說的數據庫,是指Mysql,Oracle這樣的數據庫,而不是Redis這樣的。

這裏就以Mysql舉例,這個你們應該是最熟悉的。

Mysql

Mysql緩存機制,就是緩存sql文本,及其對應的緩存結果,經過KV形式保存到Mysql服務器內存中。以後Mysql服務器,再次遇到一樣的sql語句,就會從緩存中直接返回結果,而不須要再進行sql解析,優化,執行。

可能某些人擔憂,若是數據改變了,而請求的語句是select * from xxx,那不就一直拿到舊數據了嘛。放心,mysql有這方面的處理,當對應表的數據有所修改,那麼使用了這個表的數據的緩存就所有失效。因此對於常常變更的數據表,緩存並無太大價值。

優點

  • 提高性能。一樣的語句,第一次執行可能須要1s,而第二次執行每每只須要幾毫秒。
  • 避免索引時間。由於是經過請求的sql,直接從緩存中獲取對應結果,因此沒有進行索引查詢操做。
  • 下降數據庫磁盤操做。雖然請求到達了數據庫,但若是沒有進行硬盤操做(尋道,讀取數據等),那麼該次數據庫操做對數據庫的資源消耗就小了許多(由於在數據庫中最消耗時間的就是索引操做與硬盤操做)
  • 下降數據庫資源消耗,提升查詢時間。由於其避免了數據庫得到sql後的全部操做,取而代之的是從緩存獲取數據(一個KV讀取操做,資源消耗能夠幾乎能夠忽略了)

缺點

  • mysql緩存的應用,及配置須要足夠的專業知識(通常的後端並不會很是深刻這個層次,每每須要專門的DBA進行處理)
  • mysql緩存的判斷規則不夠智能,提升了查詢緩存的使用門檻,下降了其效率
  • mysql緩存的檢查與清理須要佔用必定資源
  • mysql緩存的內存管理不夠完善,會產生必定內存碎片(貌似mysql並非直接採用數據庫的內存,就像JVM同樣。若是有不一樣意見的,能夠私信或@我。畢竟我並不擅長數據庫,雖然剛接手的工做是進行數據庫中間件開發。囧)

擴展

實際應用

在我以前接收的IOT項目中,不管是終端系統,仍是中控系統,每每都存在大數據量的數據查詢,單次的數據查詢每每涉及萬級,十萬級數據的查詢,而且可能頻繁查詢(就是屢次刷新頁面數據)。

一方面,我經過批量寫入(下降數據庫鏈接的佔用頻次),下降數據庫對應數據表的修改頻次(從原來的幾秒一次,變爲一分鐘一次)。另外一方面,進行數據庫緩存相關配置,確保在一分鐘內的數據庫不須要進行索引操做與硬盤操做,直接返回內存內的結果。從而有效提升了前端頁面數據展現效果。

固然後續,我爲了針對這一特定業務場景與需求,對業務稍作了調整,從而大大提升了數據查詢效果,大幅下降應用系統資源消耗(這個我會專門寫一篇博客,甚至專門開一個系列,用來描寫這種粒度的特定業務場景的方案設計)。

布隆過濾器

以前有人私信我,認爲布隆過濾器應該歸類於緩存架構的一部分。

我開始認爲這有必定道理,由於布隆過濾器確實涉及數據的緩存,它須要以往數據的記錄,來實現。可是後來我想了想,布隆過濾器並不該該劃分爲緩存中,由於布隆過濾器是基於緩存的,應用緩存的。就像你能夠說Redis緩存屬於緩存架構的一部分,可是你不能夠說調用緩存的應用服務器屬於緩存。因此最終,我並無將布隆過濾器劃分爲緩存的一部分。而是將它做爲一種很是有意思的過濾器,一種限流方式,一種安全手段等。

不過做爲擴展,這裏簡單說一下布隆過濾器。說白了,就是利用Hash的散列映射特性,進行數據過濾。如我在應用中設置一個數組Array(其全部值都爲0),其長度爲固定的10W。我針對每一個用戶計算一個hash值,並將這個hasn值對10W進行取餘操做,得到index值(如1000)。我將Array中第index位置的value設置爲1。這樣放在生產環境後,若是有一個用戶,其計算出來的index在Array中對應位置的值爲0,則說明這個用戶在系統中不存在(固然,若是是1,也並不能就說明其就是系統的用戶,畢竟存在哈希衝突與取餘衝突,不過幾率較低)。經過這樣的手段,有效避免無效請求等。

後續可能會專門寫一篇有關布隆過濾器的博客。

總結

以上就是緩存架構相關的知識了。固然,這些知識都是粒度比較大的,雖然我舉了一些實際例子,可是須要你們針對具體應用場景,進行調整應用。另外,這些知識都是比較通用的。可能在特定業務場景下,還有一些方案沒有列在這裏。最後,沒有最好的技術,只有最合適的技術。這裏的許多技術都須要必定的業務規模(數據量,請求數,併發量等),採用比較好的性價比,須要你們仔細考慮。

若是有什麼問題或者想法,能夠私信或@我。

願與諸君共進步。

參考

相關文章
相關標籤/搜索