首頁  >  文章  >  Java  >  java高並發系統設計之緩存篇

java高並發系統設計之緩存篇

coldplay.xixi
coldplay.xixi轉載
2020-10-10 17:24:122093瀏覽

java基礎專欄今天介紹java高並發系統設計的快取篇。

java高並發系統設計之緩存篇

常見硬體元件的延時情況如下圖:java高並發系統設計之緩存篇

從這些資料中,你可以看到,做一次內存尋址大概需要100ns,而做一次磁碟的查找則需要10ms。可見,我們使用記憶體作為快取的儲存媒體相比於以磁碟作為主要儲存媒體的資料庫來說,效能上會提高多個數量級。所以,記憶體是最常見的一種快取資料的媒體。

一、快取案例

1、TLB

Linux 記憶體管理是透過一個叫做MMU(Memory Management Unit)的硬件,來實現從虛擬位址到物理位址的轉換的,但是如果每次轉換都要做這麼複雜計算的話,無疑會造成性能的損耗,所以我們會藉助一個叫做TLB(Translation Lookaside Buffer)的組件來緩存最近轉換過的虛擬地址,和物理地址的映射。 TLB 就是一種快取元件。

2、抖音

平台上的短影片其實是使用內建的網路播放器來完成的。網路播放器接收的是資料流,將資料下載下來之後經過分離音視訊串流,解碼等流程後輸出到週邊設備上播放。播放器中通常會設計一些快取的組件,在未打開視頻時緩存一部分視頻數據,比如我們打開抖音,服務端可能一次會返回三個視頻信息,我們在播放第一個視頻的時候,播放器已經幫我們快取了第二、三個影片的部分數據,這樣在看第二個影片的時候就可以給用戶「秒開」的感覺。

3、HTTP協定快取

當我們第一次要求靜態的資源時,例如一張圖片,服務端除了回傳圖片訊息,在回應頭裡面還有一個「Etag”的字段。瀏覽器會快取圖片資訊以及這個欄位的值。當下次再要求這個圖片的時候,瀏覽器發起的請求頭裡面會有一個「If-None-Match」的字段,並且把快取的「Etag」的值寫進去發給服務端。服務端比對圖片資訊是否有變化,如果沒有,則傳回瀏覽器一個 304 的狀態碼,瀏覽器會繼續使用快取的圖片資訊。透過這種快取協商的方式,可以減少網路傳輸的資料大小,進而提升頁面展示效能。 java高並發系統設計之緩存篇

二、快取分類

1、靜態快取

靜態快取在Web 1.0 時期是非常著名的,它一般透過產生Velocity 範本或靜態HTML 文件來實現靜態緩存,在Nginx 上部署靜態緩存可以減少對於後台應用伺服器的壓力

2、分佈式緩存

分佈式緩存的大名可謂是如雷貫耳了,我們平時耳熟能詳的Memcached、Redis 就是分散式快取的典型例子。它們性能強勁,透過一些分散式的方案組成集群可以突破單機的限制。所以在整體架構中,分散式快取承擔著非常重要的角色

3、本地快取

Guava Cache 或者是Ehcache 等,它們和應用程式部署在同一個進程中,優勢是不需要跨網路調度,速度極快,所以可以用來阻擋短時間內的熱點查詢。

三、快取的讀寫策略

1、Cache Aside策略

#在更新資料時不更新緩存,而是刪除快取中的數據,在讀取數據時,發現快取中沒了數據之後,再從資料庫讀取數據,更新到快取中。 java高並發系統設計之緩存篇

這個策略就是我們使用快取最常見的策略,Cache Aside 策略(也叫旁路快取策略),這個策略資料以資料庫中的資料為準,快取中的資料是按需加載的。

Cache Aside 策略是我們日常開發中最常使用的快取策略,不過我們在使用時也要學會依情況而變,並不是一成不變的。 Cache Aside 存在的最大的問題是當寫入比較頻繁時,快取中的資料會被頻繁地清理,這會對快取的命中率有一些影響。如果你的業務對快取命中率有嚴格的要求,那麼可以考慮兩個解決方案:

一種做法是在更新資料時也更新緩存,只是在更新緩存前先加一個分散式鎖,因為這樣在同一時間只允許一個執行緒更新緩存,就不會產生並發問題了。當然這麼做對於寫入的性能會有一些影響(推薦);

另一種做法同樣也是在更新數據時更新緩存,只是給緩存加一個較短的過期時間,這樣即使出現緩存不一致的情況,快取的資料也會很快過期,對業務的影響也可以接受。

2、Read/Write Through

這個策略的核心原則是使用者只與快取打交道,由快取和資料庫通信,寫入或讀取資料。 java高並發系統設計之緩存篇

Write Through

的策略是這樣的:先查詢要寫入的資料在快取中是否已經存在,如果已經存在,則更新緩存中的數據,並且由快取元件同步更新到資料庫中,如果快取中資料不存在,我們把這種情況叫做「Write Miss(寫失效)」。一般來說,我們可以選擇兩種“Write Miss”方式:一個是“Write Allocate(按寫分配)”,做法是寫入快取對應位置,再由快取元件同步更新到資料庫中;另一個是“No -write allocate(不按寫分配)”,做法是不寫入快取中,而是直接更新到資料庫中。 我們看到 Write Through 策略中寫入資料庫是同步的,這對於效能來說會有比較大的影響,因為相較於寫入緩存,同步寫入資料庫的延遲就要高很多了。透過Write Back策略異步的更新資料庫。

Read Through

策略就簡單一些,它的步驟是這樣的:先查詢快取中資料是否存在,如果存在則直接傳回,如果不存在,則由快取元件負責從資料庫同步載入資料。

3、Write Back

這個策略的核心思想是在寫入資料時只寫入緩存,並且把快取區塊兒標記為「髒」的。而髒塊兒只有在再次使用時才會將其中的資料寫入後端儲存。 在「Write Miss」的情況下,我們採用的是「Write Allocate」的方式,也就是在寫入後端儲存的同時要寫入緩存,這樣我們在之後的寫入請求中都只需要更新快取即可,而無需更新後端儲存了。注意與上面的write through策略作區分。 java高並發系統設計之緩存篇

我們在讀取快取時如果發現快取命中則直接傳回快取資料。如果快取不命中則尋找一個可用的快取區塊兒,如果這個快取區塊兒是「髒」的,就把快取區塊兒中之前的資料寫入到後端儲存中,並且從後端儲存載入資料到快取塊兒,如果不是髒的,則由快取元件將後端儲存中的資料載入到快取中,最後我們將快取設定為不是髒的,回傳資料就好了。 java高並發系統設計之緩存篇

write back策略多用於向磁碟中寫入資料。例如:作業系統層面的 Page Cache、日誌的非同步刷盤、訊息佇列中訊息的非同步寫入磁碟等。因為這個策略在效能上的優勢毋庸置疑,它避免了直接寫磁碟造成的隨機寫入問題,畢竟寫記憶體和寫入磁碟的隨機 I/O 的延遲相差了幾個數量級呢。

四、快取高可用

快取的命中率是快取需要監控的資料指標,快取的高可用可以一定程度上減少快取穿透的機率,提升系統的穩定性。快取的高可用方案主要包括客戶端方案、中間代理層方案和服務端方案三大類:

1、客戶端方案

在客戶端方案中,你需要專注於緩存的寫和讀兩方面: 寫入資料時,需要把寫入快取的資料分散到多個節點中,即進行資料分片; 讀取資料時,可以利用多組的快取來做容錯,提升快取系統的可用性。關於讀取數據,這裡可以使用主從和多副本兩種策略,兩種策略是為了解決不同的問題而提出的。 具體的實作細節包括:資料分片、主從、多副本

資料分片

一致性Hash演算法。在這個演算法中,我們將整個 Hash 值空間組織成一個虛擬的圓環,然後將快取節點的 IP 位址或主機名稱做 Hash 取值後,就放置在這個圓環上。當我們需要確定某一個Key 需要訪問到哪個節點上的時候,先對這個Key 做同樣的Hash 取值,確定在環上的位置,然後按照順時針方向在環上“行走”,遇到的第一個快取節點就是要存取的節點。 java高並發系統設計之緩存篇

這時如果在Node 1 和Node 2 之間增加一個Node 5,你可以看到原本命中Node 2 的Key 3 現在命中到Node 5,而其它的Key 都沒有變化;同樣的道理,如果我們把Node 3 從叢集中移除,只會影響到Key 5 。所以你看,在增加和刪除節點時,只有少量的 Key 會「漂移」到其它節點上,而大部分的 Key 命中的節點還是會保持不變,從而可以保證命中率不會大幅下降。 【提示】一致性hash出現的快取雪崩現象使用虛擬節點解決。一致性hash分片與hash分片的差別在於,快取命中率的問題,hash分片在存在機器加入或是減少的情況時候,會導致快取失效,快取命中率下降。

主從

Redis 本身支援主從的部署方式,但是 Memcached 並不支持,Memcached 的主從機制是如何在客戶端實現的。為每一組 Master 設定一組 Slave,更新資料時主從同步更新。讀取時,優先從 Slave 中讀數據,如果讀取不到數據就穿透到 Master 讀取,並且將數據回種到 Slave 中以保持 Slave 數據的熱度。主從機制最大的優點就是當某一個 Slave 宕機時,還會有 Master 作為兜底,不會有大量請求穿透到資料庫的情況發生,提升了快取系統的高可用性。

多重複製

主從方式已經能夠解決大部分場景的問題,但是對於極端流量的場景下,一組Slave 通常來說並不能完全承擔所有流量,Slave 網路卡頻寬可能成為瓶頸。為了解決這個問題,我們考慮在Master/Slave 之前增加一層副本層,整體架構是這樣的:java高並發系統設計之緩存篇

這個方案中,當客戶端發起查詢請求時,請求首先會先從在多個副本群組中選取一個副本群組發起查詢,如果查詢失敗,就繼續查詢Master/Slave,並且將查詢的結果回種到所有副本群組中,避免副本群組中髒資料的存在。基於成本的考慮,每個副本組容量比 Master 和 Slave 要小,因此它只儲存了更加熱的資料。在這套架構中,Master 和 Slave 的請求量會大幅減少,為了確保它們儲存資料的熱度,在實務上我們會把 Master 和 Slave 作為一組副本組使用。

2、中間代理層

業界也有很多中間代理層方案,像是 Facebook 的Mcrouter,Twitter 的Twemproxy,豌豆莢的Codis。它們的原理基本上可以由一張圖來概括:java高並發系統設計之緩存篇

3、服務端方案

Redis 在2.4 版本中提出了Redis Sentinel 模式來解決主從Redis 部署時的高可用問題,它可以在主節點掛了以後自動將從節點提升為主節點,保證整體叢集的可用性,整體的架構如下圖所示:java高並發系統設計之緩存篇

redis Sentinel 也是叢集部署的,這樣可以避免Sentinel 節點掛掉造成無法自動故障復原的問題,每一個Sentinel 節點都是無狀態的。在Sentinel 中會配置Master 的位址,Sentinel 會時時刻刻監控Master 的狀態,當發現Master 在設定的時間間隔內無回應,就認為Master 已經掛了,Sentinel 會從從節點中選取一個提升為主節點,並且把所有其他的從節點作為新主的從節點。 Sentinel 叢集內部在仲裁的時候,會根據配置的值來決定當有幾個 Sentinel 節點認為主掛掉可以做主從切換的操作,也就是叢集內部需要對快取節點的狀態達成一致才行。

【提示】上述客戶端到sentinel叢集的連線是虛線,因為對於快取的寫入和讀取請求不會經過 Sentinel 節點。

五、快取穿透

1、帕累托

網路系統的資料存取模型一般會遵從「80/20 原則」。 「80/20 原則」又稱為帕累託法則,是義大利經濟學家帕累托提出的經濟學的理論。簡單來說,它是指在一組事物中,最重要的部分通常只佔 20%,而其他的 80% 並沒有那麼重要。把它應用到數據存取的領域,就是我們會經常存取 20% 的熱點數據,而另外的 80% 的數據則不會被經常存取。既然快取的容量有限,而且大部分的存取只會請求20% 的熱點數據,那麼理論上說,我們只需要在有限的快取空間裡儲存20% 的熱點資料就可以有效地保護脆弱的後端系統了,也就可以放棄快取另外80% 的非熱點資料了。所以這種少量的緩存穿透是不可避免的,但是對系統是沒有損害的。

2、回種空值

當我們從資料庫中查詢到空值或發生異常時,我們可以向快取中回種一個空值。但是因為空值並不是準確的業務數據,並且會佔用快取的空間,所以我們會為這個空值加一個比較短的過期時間,讓空值在短時間內能夠快速過期淘汰。回種空值雖然能夠阻擋大量穿透的請求,但如果有大量的空值緩存,也會浪費緩存的存儲空間,如果緩存空間被佔滿了,還會剔除掉一些已經被緩存的用戶信息反而會造成快取命中率的下降。所以這個方案,我建議你在使用的時候應該評估緩存容量是否能夠支撐。如果需要大量的快取節點來支持,那麼就無法透過回種空值的方式來解決,這時你可以考慮使用布隆過濾器。

3、布隆過濾器

1970 年布隆提出了一個布林過濾器的演算法,用來判斷一個元素是否在一個集合中。這種演算法由一個二進制數組和一個 Hash 演算法組成。它的基本想法如下:我們把集合中的每一個值按照提供的Hash 演算法算出對應的Hash 值,然後將Hash 值對數組長度取模後得到需要計入數組的索引值,並且將數組這個位置的值從0 改成1。在判斷一個元素是否存在於這個集合中時,你只需要將這個元素依照相同的演算法計算出索引值,如果這個位置的值為 1 就認為這個元素在集合中,否則則認為不在集合中。 java高並發系統設計之緩存篇

如何使用布隆過濾器解決快取穿透呢?

以儲存使用者資訊的表格為例進行講解。首先我們初始化一個很大的數組,比方說長度為20 億的數組,接下來我們選擇一個Hash 演算法,然後我們將目前現有的所有用戶的ID 計算出Hash 值並且映射到這個大數組中,映射位置的值設定為1,其它值設定為0。新註冊的用戶除了需要寫入資料庫之外,它也需要依照相同的演算法更新布隆過濾器的陣列中對應位置的值。那麼當我們需要查詢某一個用戶的資訊時,先查詢這個ID 在布隆過濾器中是否存在,如果不存在就直接返回空值,而不需要繼續查詢資料庫和緩存,這樣就可以大大減少異常查詢帶來的快取穿透。 java高並發系統設計之緩存篇

布隆過濾器優點:

(1)效能高。無論是寫入操作還是讀取操作,時間複雜度都是 O(1) 是常數值

(2)節省空間。例如,20 億的數組需要2000000000/8/1024/1024 = 238M 的空間,而如果使用數組來存儲,假設每個用戶ID 佔用4 個字節的空間,那麼存儲20 億用戶需要2000000000 * 4 / 1024 / 1024 = 7600M 的空間,是布隆過濾器的32 倍。

布隆過濾器缺點:

(1)它在判斷元素是否在集合中時是有一定錯誤幾率的,例如它會把不是集合中的元素判斷為處在集合中。

原因:Hash演算法本身的缺陷。

解決方案:使用多個 Hash 演算法為元素計算出多個 Hash 值,只有所有 Hash 值對應的陣列中的值都為 1 時,才會認為這個元素在集合中。

(2)不支援刪除元素。布隆過濾器不支援刪除元素的缺陷也和 Hash 碰撞有關。舉一個例子,假如兩個元素 A 和 B 都是集合中的元素,它們有相同的 Hash 值,它們就會對應到陣列的同一個位置。這時我們刪除了 A,陣列中對應位置的值也從 1 變成 0,那麼在判斷 B 的時候發現值是 0,也會判斷 B 是不在集合中的元素,就會得到錯誤的結論。

解決方案:我會讓陣列中不再只有 0 和 1 兩個值,而是儲存一個計數。例如如果 A 和 B 同時命中了一個陣列的索引,那麼這個位置的值就是 2,如果 A 被刪除了就把這個值從 2 改為 1。這個方案中的陣列不再儲存 bit 位,而是儲存數值,也會增加空間的消耗。

4、狗樁效應

比方說當有一個極熱點的緩存項,它一旦失效會有大量請求穿透到資料庫,這會對資料庫造成瞬時極大的壓力,我們把這個場景叫做「dog-pile effect」(狗樁效應)。解決狗樁效應的想法是盡量減少緩存穿透後的並發,方案也比較簡單:

(1)在程式碼中控制在某一個熱點緩存項目失效之後啟動一個後台線程,穿透到資料庫,將資料載入到快取中,在快取未載入之前,所有存取這個快取的請求都不再穿透而直接返回。

(2)透過在Memcached 或Redis 中設定分散式鎖,只有取得到鎖的請求才能夠穿透到資料庫

六、CDN

##1、靜態資源加速的原因

在我們的系統中存在著大量的靜態資源請求:對於行動APP 來說,這些靜態資源主要是圖片、影片和串流媒體資訊;對於Web 網站來說,則包括了JavaScript 檔案、CSS 檔案、靜態HTML 檔案等等。它們的讀取請求量極大並且對訪問速度的要求很高還佔據了很高的頻寬,這時會出現訪問速度慢頻寬被佔滿影響動態請求的問題,那麼你就需要考慮如何針對這些靜態資源進行讀加速了。

2、CDN

靜態資源存取的關鍵點是就近訪問,即北京用戶訪問北京的數據,杭州用戶訪問杭州的數據,這樣才可以達到性能的最優。我們考慮在業務伺服器的上層增加一層特殊的緩存,用來承擔絕大部分對於靜態資源的訪問,這一層特殊緩存的節點需要遍布在全國各地,這樣可以讓用戶選擇最近的節點訪問。快取的命中率也需要一定的保證,盡量減少存取資源儲存源站的請求數量(回源請求)。這一層快取就是CDN。

CDN(Content Delivery Network/Content Distribution Network,內容傳遞網路)。簡單來說,CDN 是將靜態的資源分發到位於多個地理位置機房中的伺服器上,因此它能很好地解決資料就近存取的問題,也就加快了靜態資源的存取速度。

3、建構CDN系統

建構一個CDN 系統需要考慮哪兩點:

(1)如何將使用者的請求對應到CDN 節點上

你可能會覺得這很簡單啊,只需要告訴用戶CDN 節點的IP 位址,然後請求這個IP 位址上面部署的CDN 服務就可以了啊。但是,並不是這樣,需要把ip替換為對應的網域。那麼如何做到這一點呢?這就需要依賴 DNS 來幫我們解決網域對映的問題了。 DNS(Domain Name System,網域名稱系統)其實就是一個儲存網域名稱和 IP 位址對應關係的分散式資料庫。而域名解析的結果一般有兩種,一種叫做“A 記錄”,返回的是域名對應的IP 位址;另一種是“CNAME 記錄”,返回的是另一個域名,也就是說當前域名的解析要跳到另一個網域的解析上。

舉個例子:例如你的公司的一級網域叫做example.com,那麼你可以把你的圖片服務的網域定義為“img.example.com”,然後將這個網域的解析結果的CNAME 配置到CDN 提供的網域上,例如uclound 可能會提供一個網域是「80f21f91.cdn.ucloud.com.cn」這個網域。這樣你的電商系統使用的圖片位址可以是「img.example.com/1.jpg」。

使用者在要求這個位址時,DNS 伺服器會將網域名稱解析到80f21f91.cdn.ucloud.com.cn 網域上,然後再將這個網域解析為CDN 的節點IP,這樣就可以得到CDN 上面的資源數據了。

網域層級解析最佳化

因為網域解析過程是分級的,每一層有專門的網域名稱伺服器承擔解析的職責,所以網域的解析過程有可能需要跨越公網做多次DNS 查詢,在效能上是比較差的。一個解決的想法是:在 APP 啟動時對需要解析的網域做預先解析,然後把解析的結果快取到本地的一個 LRU 快取裡面。這樣當我們要使用這個網域的時候,只需要從快取直接拿到所需要的 IP 位址就好了,如果快取中不存在才會走整個 DNS 查詢的過程。同時為了避免 DNS 解析結果的變更造成快取內資料失效,我們可以啟動計時器定期更新快取中的資料。

(2)如何根據使用者的地理位置資訊選擇到比較近的節點。

GSLB(Global Server Load Balance,全域負載平衡)的意思是對於部署在不同地域的伺服器之間做負載平衡,下面可能管理了很多的本地負載平衡元件。它有兩方面的作用:一方面,它是一種負載平衡伺服器,負載平衡,顧名思義嘛,指的是讓流量平均分配使得下面管理的伺服器的負載更平均;另一方面,它還需要保證流量流經的伺服器與流量源頭在地緣上是比較接近的。

GSLB 可以透過多種策略來保證傳回的CDN 節點和使用者盡量保證在同一地緣區域,比如說可以將使用者的IP 位址依照地理位置劃分為若干個區域,然後將CDN 節點對應到在一個區域上,根據使用者所在區域來傳回適當的節點;也可以透過發送資料包測量RTT 的方式來決定傳回哪一個節點。

總結:DNS 技術是CDN 實作中使用的核心技術,可以將使用者的請求映射到CDN 節點上;DNS 解析結果需要做本地緩存,降低DNS 解析過程的回應時間;GSLB 可以給用戶回傳一個離他更近的節點,加快靜態資源的存取速度。

拓展

(1)百度網域的解析過程

一開始,網域解析請求先會檢查本機的hosts 文件,查看是否有www.baidu.com 對應的IP;如果沒有的話,就請求Local DNS 是否有域名解析結果的緩存,如果有就返回標識是從非權威DNS 返回的結果;如果沒有就開始DNS 的迭代查詢。先請求根DNS,根DNS 回傳頂級DNS(.com)的位址;再請求.com 頂級DNS 得到baidu.com 的網域名稱伺服器位址;再從baidu.com 的網域伺服器查詢到www.baidu.com 對應的IP 位址,傳回這個IP 位址的同時標記這個結果是來自於權威DNS 的結果,同時寫入Local DNS 的解析結果緩存,這樣下一次的解析同一個網域就不需要做DNS 的迭代查詢了。

(2)CDN延時

一般我們會透過CDN 廠商的介面將靜態的資源寫入到某一個CDN 節點上,再由CDN 內部的同步機制將資源分散同步到每個CDN 節點,即使CDN 內部網路經過了最佳化,這個同步的過程是有延時的,一旦我們無法從選定的CDN 節點上獲取到數據,我們就不得不從來源站獲取數據,而用戶網路到源站的網路可能會跨越多個主幹網,這樣不僅性能上有損耗也會消耗源站的頻寬,帶來更高的研發成本。所以我們在使用 CDN 的時候需要注意 CDN 的命中率和源站的頻寬情況。

相關學習推薦:java基礎

以上是java高並發系統設計之緩存篇的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:juejin.im。如有侵權,請聯絡admin@php.cn刪除