Redis作為緩存應(yīng)用的情形詳細分析
為什么使用緩存
Redis是一個內(nèi)存型數(shù)據(jù)庫,也就是說,所有的數(shù)據(jù)都會存在與內(nèi)存中,基于Redis的高性能特性,我們將Redis用在緩存場景非常廣泛。使用起來方便,響應(yīng)也是遠超關(guān)系型數(shù)據(jù)庫。
應(yīng)用場景
Redis的應(yīng)用場景非常廣泛。雖然Redis是一個key-value的內(nèi)存數(shù)據(jù)庫,但在實際場景中,Redis經(jīng)常被作為緩存來使用,如面對數(shù)據(jù)高并發(fā)的讀寫、海量數(shù)據(jù)的讀寫等。
舉個例子,A網(wǎng)站首頁一天有100萬人訪問,其中有一個“積分商城”的板塊,要直接從數(shù)據(jù)庫查詢,那么一天就要多消耗100萬次數(shù)據(jù)庫請求。如果將這些數(shù)據(jù)儲存到Redis(內(nèi)存)中,要用的時候,直接從內(nèi)存調(diào)取,不僅可以大大節(jié)省系統(tǒng)直接讀取磁盤來獲得數(shù)據(jù)的IO開銷,提高服務(wù)器的資源利用率,還能極大地提升速度。
比如很多大型電商網(wǎng)站、視頻網(wǎng)站和游戲應(yīng)用等,存在大規(guī)模數(shù)據(jù)訪問,對數(shù)據(jù)查詢效率要求高。Redis服務(wù)可實現(xiàn)頁面緩存、應(yīng)用緩存、狀態(tài)緩存、事件并行處理,能夠有效減少數(shù)據(jù)庫磁盤IO,提高數(shù)據(jù)查詢效率,減輕管理維護工作量,降低數(shù)據(jù)庫存儲成本。對傳統(tǒng)磁盤數(shù)據(jù)庫是一個重要的補充,成為了互聯(lián)網(wǎng)應(yīng)用,尤其是支持高并發(fā)訪問的互聯(lián)網(wǎng)應(yīng)用必不可少的基礎(chǔ)服務(wù)之一。
具體而言,分布式緩存Redis可用于以下場景:
1、頁面緩存
Redis可將Web頁面的內(nèi)容片段,包括HTML,CSS和圖片等靜態(tài)數(shù)據(jù),緩存到Redis實例,提高網(wǎng)站的訪問性能。
比如在電商類應(yīng)用中,熱銷商品展示、秒殺推薦等數(shù)據(jù)面臨高并發(fā)讀的壓力,分布式緩存Redis的高并發(fā)及靈活擴展,可輕松支持此類應(yīng)用。
2、狀態(tài)緩存
Redis可將Session會話狀態(tài)及應(yīng)用橫向擴展時的狀態(tài)數(shù)據(jù)等緩存到DCS實例,實現(xiàn)狀態(tài)數(shù)據(jù)共享。在應(yīng)對游戲應(yīng)用中爆發(fā)式增長的玩家數(shù)據(jù)存儲和讀寫請求時,使用分布式緩存Redis可通過將熱點數(shù)據(jù)放入緩存,加快用戶端訪問速度,提升用戶體驗。
3、應(yīng)用對象緩存
Redis可作為服務(wù)層的二級緩存對外提供服務(wù),減輕數(shù)據(jù)庫的負載壓力,加速應(yīng)用訪問。
4、事件緩存
Redis可提供針對事件流的連續(xù)查詢(continuous query)處理技術(shù),滿足實時性需求。
使用緩存的收益和成本
如圖左側(cè)為客戶端直接調(diào)用存儲層的架構(gòu),右側(cè)為比較典型的緩存層+存儲層架構(gòu),下面分析一下緩存加入后帶來的收益和成本。
收益:
l 加速讀寫:因為緩存通常都是全內(nèi)存的,而存儲層通常讀寫性能不夠強悍(例如MySQL),通過緩存的使用可以有效地加速讀寫,優(yōu)化用戶體驗。
l 降低后端負載:幫助后端減少訪問量和復雜計算(例如很復雜的SQL語句),在很大程度降低了后端的負載。
成本:
l 數(shù)據(jù)不一致性:緩存層和存儲層的數(shù)據(jù)存在著一定時間窗口的不一致性,時間窗口跟更新策略有關(guān)。
l 代碼維護成本:加入緩存后,需要同時處理緩存層和存儲層的邏輯,增大了開發(fā)者維護代碼的成本。
l 運維成本:以Redis Cluster為例,加入后無形中增加了運維成本。
緩存不一致
一致性
1、強一致性
如果你的項目對緩存的要求是強一致性的,那么請不要使用緩存。這種一致性級別是最符合用戶直覺的,它要求系統(tǒng)寫入什么,讀出來的也會是什么,用戶體驗好,但實現(xiàn)起來往往對系統(tǒng)的性能影響大。
2、弱一致性
這種一致性級別約束了系統(tǒng)在寫入成功后,不承諾立即可以讀到寫入的值,也不承諾多久之后數(shù)據(jù)能夠達到一致,但會盡可能地保證到某個時間級別(比如秒級別)后,數(shù)據(jù)能夠達到一致狀態(tài)
3**、最終一致性**
最終一致性是弱一致性的一個特例,系統(tǒng)會保證在一定時間內(nèi),能夠達到一個數(shù)據(jù)一致的狀態(tài)。這里之所以將最終一致性單獨提出來,是因為它是弱一致性中非常推崇的一種一致性模型,也是業(yè)界在大型分布式系統(tǒng)的數(shù)據(jù)一致性上比較推崇的模型。一般情況下,高可用只確保最終一致性,不確保強一致性。
強一致性,讀請求和寫請求會串行化,串到一個內(nèi)存隊列里去,這樣會大大增加系統(tǒng)的處理效率,吞吐量也會大大降低。
業(yè)務(wù)場景
在絕大多數(shù)的系統(tǒng)中數(shù)據(jù)庫往往是用戶并發(fā)訪問最薄弱的地方,并且在高并發(fā)下的讀多寫少的情況下,我們往往會借助一些中間鍵,來解決數(shù)據(jù)訪問過大時造成的數(shù)據(jù)庫宕機情況,例如我們可以使用Redis來作為緩存,讓請求先訪問到Redis,而不是直接訪問數(shù)據(jù)庫。而在這種業(yè)務(wù)場景下,可能會出現(xiàn)緩存和數(shù)據(jù)庫數(shù)據(jù)不一致性的問題。
問題產(chǎn)生的原因
一般來說讀取緩存步驟是不會有什么問題的,但是一旦涉及到數(shù)據(jù)更新,也就是數(shù)據(jù)庫和緩存都操作,就容易出現(xiàn)緩存(Redis)和數(shù)據(jù)庫(MySQL)間的數(shù)據(jù)一致性問題。
在數(shù)據(jù)更新時,我們需要做以下兩步:
- 操作MySQL
- 操作緩存
但是無論是先執(zhí)行步驟1還是先執(zhí)行步驟2,都有可能出現(xiàn)數(shù)據(jù)不一致的情況,主要是因為讀寫是并發(fā)的,我們無法保證他們的先后順序。
相關(guān)策略
先做一個說明,從理論上來說,給緩存設(shè)置過期時間,是保證最終一致性的解決方案(如果要求強一致性的話,我認為沒有必要添加緩存了,直接走數(shù)據(jù)庫)。這種前提下,我們可以對存入緩存的數(shù)據(jù)設(shè)置過期時間,所有的寫操作以數(shù)據(jù)庫為準,對緩存操作只是盡最大努力即可。也就是說如果數(shù)據(jù)庫寫成功,緩存更新失敗,那么只要到達過期時間,則后面的讀請求自然會從數(shù)據(jù)庫中讀取新值然后回填緩存。因此,接下來討論的思路不依賴于給緩存設(shè)置過期時間這個方案。
給出了三種更新策略:
- 先更新數(shù)據(jù)庫,再更新緩存
- 先刪除緩存,再更新數(shù)據(jù)庫
- 先更新數(shù)據(jù)庫,在刪除緩存
先更新數(shù)據(jù)庫值再更新緩存值
最不可能選擇的策略,原因是此種策略可能會在線程安全的角度和業(yè)務(wù)場景角度生成臟數(shù)據(jù)和性能問題。
原因一:線程安全的角度
同時有請求A和請求B進行更新操作,那么就會出現(xiàn)
- 請求A更新數(shù)據(jù)庫
- 請求B更新數(shù)據(jù)庫
- 請求B更新緩存
- 請求A更新緩存
這就出現(xiàn)請求A更新緩存應(yīng)該比請求B更新緩存早才對,但是因為網(wǎng)絡(luò)等原因,B比A更早更新了緩存。這就導致了臟數(shù)據(jù),因此不考慮。
業(yè)務(wù)場景角度
(1)如果是寫數(shù)據(jù)庫場景比較多,而讀數(shù)據(jù)場景比較少的業(yè)務(wù)需求,那么采用這種方案就會導致,數(shù)據(jù)壓根還沒讀到,緩存就被頻繁的更新,浪費性能,緩存此類數(shù)據(jù),沒有很大的意義。
(2)如果是寫入數(shù)據(jù)庫的值,并不是直接寫入緩存的,而是要經(jīng)過一系列復雜的計算再寫入緩存。那么,每次寫入數(shù)據(jù)庫后,都再次計算寫入緩存的值,無疑是浪費性能的。顯然,刪除緩存更為適合。
后面兩種策略都是對緩存進行刪除,這里先做一個解釋。
例子:數(shù)據(jù)庫在1小時內(nèi)更新1000次那么緩存也更新1000次,但是這個緩存可能在1小時內(nèi)只被讀了1次,那么就沒有必要更新1000次了。反過來,如果是刪除的話,那么也只是做了1次刪除操作,當緩存真正被讀取的時候才去更新。
刪除緩存值再更新數(shù)據(jù)庫值
- 請求A進行更新操作,首先刪除緩存
- 請求B查詢發(fā)現(xiàn)緩存不存在
- 請求B去數(shù)據(jù)庫查詢得到舊值
- 請求B將舊值寫入緩存
- 請求A將新值寫入數(shù)據(jù)庫
上述情況就會導致不一致的情形出現(xiàn)。而且,如果不采用給緩存設(shè)置過期時間策略,該數(shù)據(jù)永遠都是臟數(shù)據(jù)。我們可以采用延遲雙刪策略,來解決這個問題。
相對應(yīng)的步驟:
- 先淘汰緩存
- 再寫數(shù)據(jù)庫
- 休眠t秒,再次淘汰緩存
這么做的目的,就是確保讀請求結(jié)束,寫請求可以刪除讀請求造成的緩存臟數(shù)據(jù)。
// 偽代碼 public void updateDb(String key,Object data) { redis.delKey(key); db.updateData(data); Thread.sleep(t); redis.delKey(key); }
如果系統(tǒng)中MySQL使用了讀寫分離模式,那么有可能會出現(xiàn)在主從同步?jīng)]有完成時,讀請求就去讀取數(shù)據(jù)了,這時候就會讀取到舊值,這里我們可以延長睡眠時間,讓主從同步完成后在進行一次刪除(如果不考慮主從的情況下,采用雙刪不用加延時時間也是可以保證一直性的)。
先更新數(shù)據(jù)庫值在刪除緩存值
假設(shè)有兩個請求,請求A進行更新操作,請求B進行查詢操作。
那么會出現(xiàn)如下情形:
- 請求A進行更新操作,首先更新數(shù)據(jù)庫
- 請求B進行查詢操作,擊中緩存,得到舊值
- 請求A進行刪除緩存操作
在這種情況下如果其他線程并發(fā)讀緩存的請求不多,那么,就不會有很多請求讀取到舊值。而且,請求 A 一般也會很快刪除緩存值,這樣一來,其他線程再次讀取時,就會發(fā)生緩存缺失,進而從數(shù)據(jù)庫中讀取最新值。所以,這種情況對業(yè)務(wù)的影響較小。
無論是策略2還是策略3都有可能會出現(xiàn)這種情況:刪除緩存失敗,這時我們可以采用重試機制來保證數(shù)據(jù)的一致性。
方案的詳細設(shè)計
在相關(guān)策略的調(diào)用中,雖然提出了一些簡單解決方案,但是沒有考慮到列如 緩存刪除失敗,數(shù)據(jù)庫更新失敗等情況,因此需要增加重試策略,但是還是可能會出現(xiàn)比較不一致的問題,此處詳細介紹幾種方案。
流程如下:
- 更新數(shù)據(jù)庫數(shù)據(jù);
- 緩存因為種種問題刪除失敗
- 將需要刪除的key發(fā)送至消息隊列
- 自己消費消息,獲得需要刪除的key
- 繼續(xù)重試刪除操作,直到成功
如果能夠成功地刪除或更新,我們就要把這些值從消息隊列中去除,以免重復操作,此時,我們也可以保證數(shù)據(jù)庫和緩存的數(shù)據(jù)一致了。否則的話,我們還需要再次進行重試。如果重試超過的一定次數(shù),還是沒有成功,我們就需要向業(yè)務(wù)層發(fā)送報錯信息了。
// 偽代碼 public void updateDb(String key,Object data){ db.updateData(data); if (!redis.delKey(key)) { mq.send(key); new Thread(() -> asyncDel()).start(); } } // 異步重試 private void asyncDel() { int count = 0; String key = mq.get(); while(!redis.delKey(key)) { count++; if (count > 5) { throw new DelFailException(); } } mq.remove(key); }
這種雖然可以解決,但是會對業(yè)務(wù)代碼造成侵入,而且還需要去維護消息隊列,如果可以容忍的話,我覺得是可選的方案之一。
注意 需要使用有序的消息隊列,保證消息的有序性。重試刪除
訂閱binlog
業(yè)務(wù)代碼只會操作數(shù)據(jù)庫,不操作緩存。同時啟動一個訂閱binlog的程序去監(jiān)聽刪除操作,然后投遞到消息隊列中。再啟動一個消費者,根據(jù)消息去刪除緩存。
canal是用來模擬MySQL slave,來訂閱MySQL master 的binlog。
異步重試
總結(jié)
對于讀多寫少的數(shù)據(jù),請使用緩存。
為了保持數(shù)據(jù)庫和緩存的一致性,會導致系統(tǒng)吞吐量的下降。
為了保持數(shù)據(jù)庫和緩存的一致性,會導致業(yè)務(wù)代碼邏輯復雜。
緩存做不到絕對一致性,但可以做到最終一致性。
對于需要保證緩存數(shù)據(jù)庫數(shù)據(jù)一致的情況,請盡量考慮對一致性到底有多高要求,選定合適的方案,避免過度設(shè)計。
緩存問題
緩存穿透
問題描述
緩存穿透是指查詢一個根本不存在的數(shù)據(jù),緩存層和存儲層都不會命中,通常出于容錯的考慮,如果從存儲層查不到數(shù)據(jù)則不寫入緩存層,如下圖所示
整個過程分為如下3步:
緩存層不命中。存儲層不命中,不將空結(jié)果寫回緩存。返回空結(jié)果。
緩存穿透將導致不存在的數(shù)據(jù)每次請求都要到存儲層去查詢,失去了緩存保護后端存儲的意義。
緩存穿透問題可能會使后端存儲負載加大,由于很多后端存儲不具備高并發(fā)性,甚至可能造成后端存儲宕掉。通??梢栽诔绦蛑蟹謩e統(tǒng)計總調(diào)用數(shù)、緩存層命中數(shù)、存儲層命中數(shù),如果發(fā)現(xiàn)大量存儲層空命中,可能就是出現(xiàn)了緩存穿透問題。
解決方案
造成緩存穿透的基本原因有兩個。第一,自身業(yè)務(wù)代碼或者數(shù)據(jù)出現(xiàn)問題,第二,一些惡意攻擊、爬蟲等造成大量空命中。下面我們來看一下如何解決緩存穿透問題。
緩存空對象
如圖所示,當?shù)?步存儲層不命中后,仍然將空對象保留到緩存層中,之后再訪問這個數(shù)據(jù)將會從緩存中獲取,這樣就保護了后端數(shù)據(jù)源。
緩存空對象會有兩個問題:第一,空值做了緩存,意味著緩存層中存了更多的鍵,需要更多的內(nèi)存空間,比較有效的方法是針對這類數(shù)據(jù)設(shè)置一個較短的過期時間,讓其自動剔除。第二,緩存層和存儲層的數(shù)據(jù)會有一段時間窗口的不一致,可能會對業(yè)務(wù)有一定影響。例如過期時間設(shè)置為5分鐘,如果此時存儲層添加了這個數(shù)據(jù),那此段時間就會出現(xiàn)緩存層和存儲層數(shù)據(jù)的不一致,此時可以利用消息系統(tǒng)或者其他方式清除掉緩存層中的空對象。
布隆過濾器攔截
布隆過濾器:實際上是一個很長的二進制向量和一系列隨機映射函數(shù)。布隆過濾器可以用于檢索一個元素是否在一個集合中。它的優(yōu)點是空間效率和查詢時間都比一般的算法要好的多,缺點是有一定的誤識別率和刪除困難??梢愿嬖V你某樣東西一定不存在或者可能存在。
如圖所示,在訪問緩存層和存儲層之前,將存在的key用布隆過濾器提前保存起來,做第一層攔截。例如:一個推薦系統(tǒng)有4億個用戶id,每個小時算法工程師會根據(jù)每個用戶之前歷史行為計算出推薦數(shù)據(jù)放到存儲層中,但是最新的用戶由于沒有歷史行為,就會發(fā)生緩存穿透的行為,為此可以將所有推薦數(shù)據(jù)的用戶做成布隆過濾器。如果布隆過濾器認為該用戶id不存在,那么就不會訪問存儲層,在一定程度保護了存儲層。
兩種方案比對
緩存雪崩
如圖描述了什么是緩存雪崩:由于緩存層承載著大量請求,有效地保護了存儲層,但是如果緩存層由于某些原因不能提供服務(wù),于是所有的請求都會達到存儲層,存儲層的調(diào)用量會暴增,造成存儲層也會級聯(lián)宕機的情況。
預防和解決緩存雪崩問題,可以從以下三個方面進行著手。
(1) 保證緩存層服務(wù)高可用性。如果緩存層設(shè)計成高可用的,即使個別節(jié)點、個別機器、甚至是機房宕掉,依然可以提供服務(wù),例如前面介紹過的Redis Sentinel和Redis Cluster都實現(xiàn)了高可用。
(2) 依賴隔離組件為后端限流并降級。無論是緩存層還是存儲層都會有出錯的概率,可以將它們視同為資源。作為并發(fā)量較大的系統(tǒng),假如有一個資源不可用,可能會造成線程全部阻塞在這個資源上,造成整個系統(tǒng)不可用。降級機制在高并發(fā)系統(tǒng)中是非常普遍的。實際項目中,我們需要對重要的資源(例如Redis、MySQL、HBase、外部接口)都進行隔離,讓每種資源都單獨運行在自己的線程池中,即使個別資源出現(xiàn)了問題,對其他服務(wù)沒有影響。但是線程池如何管理,比如如何關(guān)閉資源池、開啟資源池、資源池閥值管理,這些做起來還是相當復雜的。這里推薦使用Java依賴隔離工具Hystrix,他是解決依賴隔離的利器。
(3) 提前演練。在項目上線前,演練緩存層宕掉后,應(yīng)用以及后端的負載情況以及可能出現(xiàn)的問題,在此基礎(chǔ)上做一些預案設(shè)定。
緩存擊穿(熱點數(shù)據(jù)集中失效)
問題描述
當一個key是熱點key,并發(fā)量很大,而且重建緩存不能在短時間完成,在緩存失效的一瞬間,就會有大量的線程來重建緩存,造成后端負載加大,甚至讓應(yīng)用崩潰,這就叫緩存擊穿。如下圖:
解決方案
互斥鎖
此方法只允許一個線程重建緩存,其他線程等待重建緩存的線程執(zhí)行完,重新從緩存獲取數(shù)據(jù)即可,整個過程如圖所示。
永遠不過期
“永遠不過期”包含兩層意思:
l 從緩存層面來看,確實沒有設(shè)置過期時間,所以不會出現(xiàn)熱點key過期后產(chǎn)生的問題,也就是“物理”不過期。
l 從功能層面來看,為每個value設(shè)置一個邏輯過期時間,當發(fā)現(xiàn)超過邏輯過期時間后,會使用單獨的線程去構(gòu)建緩存。
整個過程如圖所示:
此方法有效杜絕了熱點key產(chǎn)生的問題,但唯一不足的就是重構(gòu)緩存期間,會出現(xiàn)數(shù)據(jù)不一致的情況,這取決于應(yīng)用方是否容忍這種不一致。
兩種方案對比
到此這篇關(guān)于Redis作為緩存應(yīng)用的情形詳細分析的文章就介紹到這了,更多相關(guān)Redis作為緩存內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
java實現(xiàn)大文件導出的實現(xiàn)與優(yōu)化
這篇文章主要為大家詳細介紹了java實現(xiàn)大文件導出的實現(xiàn)與優(yōu)化的相關(guān)資料,文中的示例代碼講解詳細,對我們深入了解java有一定的幫助,感興趣的小伙伴可以了解下2023-11-11Java 實戰(zhàn)范例之線上婚紗攝影預定系統(tǒng)的實現(xiàn)
讀萬卷書不如行萬里路,只學書上的理論是遠遠不夠的,只有在實戰(zhàn)中才能獲得能力的提升,本篇文章手把手帶你用java+javaweb+SSM+springboot+mysql實現(xiàn)一個線上婚紗攝影預定系統(tǒng),大家可以在過程中查缺補漏,提升水平2021-11-11mybatis-plus中l(wèi)ambdaQuery()與lambdaUpdate()比較常見的使用方法總結(jié)
mybatis-plus是在mybatis的基礎(chǔ)上做增強不做改變,簡化了CRUD操作,下面這篇文章主要給大家介紹了關(guān)于mybatis-plus中l(wèi)ambdaQuery()與lambdaUpdate()比較常見的使用方法,需要的朋友可以參考下2022-09-09java對象數(shù)組實現(xiàn)學生信息管理系統(tǒng)
這篇文章主要為大家詳細介紹了java對象數(shù)組實現(xiàn)學生信息管理系統(tǒng),文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-06-06IDEA創(chuàng)建Maven項目后報錯不出現(xiàn)src文件夾的情況解決
最近剛開始學習maven,正準備使用idea創(chuàng)建一個maven項目練手,卻發(fā)現(xiàn)自己創(chuàng)建的maven項目始終沒有src目錄,下面這篇文章主要給大家介紹了關(guān)于IDEA創(chuàng)建Maven項目后報錯不出現(xiàn)src文件夾的情況解決,需要的朋友可以參考下2023-05-05