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

来源:https://www.cnblogs.com/Tiancheng-Duan/archive/2020/01/13/12185507.html
-Advertisement-
Play Games

緩存中間件 緩存架構的實現(下) 前言 緩存架構,說白了就是利用各種手段,來實現緩存,從而降低伺服器,乃至資料庫的壓力。 這裡把之前提出的緩存架構的技術分類放出來: 瀏覽器緩存 Cookie LocalStorage SessionStorage CDN緩存 負載層緩存 Nginx緩存模塊 Squi ...


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

前言

緩存架構,說白了就是利用各種手段,來實現緩存,從而降低伺服器,乃至資料庫的壓力。

這裡把之前提出的緩存架構的技術分類放出來:

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

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

應用層緩存

應用層的緩存,往往用戶的請求最終達到了應用伺服器,但是未達到資料庫,其涉及應用伺服器的具體開發。

Etag

之所以將Etag技術放在應用層緩存,是因為用戶的請求必定達到應用層。

Etag的意思就是,如果連續兩次請求的請求內容是一致的,那麼兩次響應也應該是一致的。那麼第一次請求的響應,就可以充當第二次請求的響應。

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

那麼應用伺服器怎麼判斷兩次請求一致呢。它可以通過兩次請求的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,也並不能就說明其就是系統的用戶,畢竟存在哈希衝突與取餘衝突,不過概率較低)。通過這樣的手段,有效避免無效請求等。

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

總結

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

如果有什麼問題或者想法,可以私信或@我。

願與諸君共進步。

參考


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

-Advertisement-
Play Games
更多相關文章
  • <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv ...
  • 淺拷貝 拷貝就是複製, 就相當於把一個對象中的所有的內容, 複製一份給另一個對象, 直接複製, 或者說, 就是把一個對象的地址給了另一個對象, 他們指向相同, 兩個對象之間有共同的屬性或者方法, 都可以使用 寫一個函數,作用:把一個對象的屬性複製到另一個對象中,淺拷貝 var obj1={ age: ...
  • Vue 嵌套路由使用總結 by:授客 QQ:1033553122 開發環境 Win 10 node-v10.15.3-x64.msi 下載地址: https://nodejs.org/en/ 需求場景 如下圖,我們希望點擊導航欄不同菜單時,導航欄下方載入不同的組件,進而展示不同的頁面內容 解決方案 ...
  • 課程介紹 淺拷貝 深拷貝 | >遞歸 遍歷DOM樹 | >遞歸 晚上能夠把代碼寫出來是最好的 正則表達式 很重要的東西 元字元 寫幾個正則表達式 寫代碼 正則表達式的案例 >代碼寫出來 數組和偽數組的區別 複習 apply和call方法的使用和區別 都可以改變this指向的 使用方式: 函數名.ap ...
  • 話說印度研發了最新款的智能機器人,代號“七弟”,用於執行特殊任務。 由於開發者的大意疏忽,七弟的內核程式中存在一個隱晦的bug:當周圍播放電子音樂時,電子音樂中強烈且帶節奏的聲波會影響七弟周圍的空氣密度,進而干擾裡面電子元件的電容電壓值,當電容釋放時會執行一段固定的步行程式。但是電音中的節拍時長限制 ...
  • 遞歸案例 遞歸案例: 求一個數字各個位數上的數字的和: 123 >6 1+2+3 //遞歸案例:求一個數字各個位數上的數字的和: 123 >6 1+2+3 function getEverySum(x) { if (x < 10) { return x; } //獲取的是這個數字的個位數 retur ...
  • 遞歸 遞歸: 函數中調用函數自己, 此時就是遞歸, 遞歸一定要有結束的條件 var i = 0; function f1() { i++; if (i < 5) { f1(); } console.log("從前有個山,山裡有個廟,廟裡有個和尚給小和尚講故事"); } f1(); ...
  • 最近看了《Head First Design Patterns》這本書。正如其名,這本書講的是設計模式(Design Patterns),而這本書的第一章,講的是很重要的一些設計原則(Design Principles)。 Identify the aspects of your applicati ...
一周排行
    -Advertisement-
    Play Games
  • 概述:在C#中,++i和i++都是自增運算符,其中++i先增加值再返回,而i++先返回值再增加。應用場景根據需求選擇,首碼適合先增後用,尾碼適合先用後增。詳細示例提供清晰的代碼演示這兩者的操作時機和實際應用。 在C#中,++i 和 i++ 都是自增運算符,但它們在操作上有細微的差異,主要體現在操作的 ...
  • 上次發佈了:Taurus.MVC 性能壓力測試(ap 壓測 和 linux 下wrk 壓測):.NET Core 版本,今天計劃準備壓測一下 .NET 版本,來測試並記錄一下 Taurus.MVC 框架在 .NET 版本的性能,以便後續持續優化改進。 為了方便對比,本文章的電腦環境和測試思路,儘量和... ...
  • .NET WebAPI作為一種構建RESTful服務的強大工具,為開發者提供了便捷的方式來定義、處理HTTP請求並返迴響應。在設計API介面時,正確地接收和解析客戶端發送的數據至關重要。.NET WebAPI提供了一系列特性,如[FromRoute]、[FromQuery]和[FromBody],用 ...
  • 原因:我之所以想做這個項目,是因為在之前查找關於C#/WPF相關資料時,我發現講解圖像濾鏡的資源非常稀缺。此外,我註意到許多現有的開源庫主要基於CPU進行圖像渲染。這種方式在處理大量圖像時,會導致CPU的渲染負擔過重。因此,我將在下文中介紹如何通過GPU渲染來有效實現圖像的各種濾鏡效果。 生成的效果 ...
  • 引言 上一章我們介紹了在xUnit單元測試中用xUnit.DependencyInject來使用依賴註入,上一章我們的Sample.Repository倉儲層有一個批量註入的介面沒有做單元測試,今天用這個示例來演示一下如何用Bogus創建模擬數據 ,和 EFCore 的種子數據生成 Bogus 的優 ...
  • 一、前言 在自己的項目中,涉及到實時心率曲線的繪製,項目上的曲線繪製,一般很難找到能直接用的第三方庫,而且有些還是定製化的功能,所以還是自己繪製比較方便。很多人一聽到自己畫就害怕,感覺很難,今天就分享一個完整的實時心率數據繪製心率曲線圖的例子;之前的博客也分享給DrawingVisual繪製曲線的方 ...
  • 如果你在自定義的 Main 方法中直接使用 App 類並啟動應用程式,但發現 App.xaml 中定義的資源沒有被正確載入,那麼問題可能在於如何正確配置 App.xaml 與你的 App 類的交互。 確保 App.xaml 文件中的 x:Class 屬性正確指向你的 App 類。這樣,當你創建 Ap ...
  • 一:背景 1. 講故事 上個月有個朋友在微信上找到我,說他們的軟體在客戶那邊隔幾天就要崩潰一次,一直都沒有找到原因,讓我幫忙看下怎麼回事,確實工控類的軟體環境複雜難搞,朋友手上有一個崩潰的dump,剛好丟給我來分析一下。 二:WinDbg分析 1. 程式為什麼會崩潰 windbg 有一個厲害之處在於 ...
  • 前言 .NET生態中有許多依賴註入容器。在大多數情況下,微軟提供的內置容器在易用性和性能方面都非常優秀。外加ASP.NET Core預設使用內置容器,使用很方便。 但是筆者在使用中一直有一個頭疼的問題:服務工廠無法提供請求的服務類型相關的信息。這在一般情況下並沒有影響,但是內置容器支持註冊開放泛型服 ...
  • 一、前言 在項目開發過程中,DataGrid是經常使用到的一個數據展示控制項,而通常表格的最後一列是作為操作列存在,比如會有編輯、刪除等功能按鈕。但WPF的原始DataGrid中,預設只支持固定左側列,這跟大家習慣性操作列放最後不符,今天就來介紹一種簡單的方式實現固定右側列。(這裡的實現方式參考的大佬 ...