前言
最近在回顧&學習Redis相關的知識,也是本着分享的態度和知識點的記錄在這裏寫下相關的文章。希望能幫助各位讀者學習或者回顧到Redis相關的知識,當然,本人才疏學淺,在學習和寫作的過程中難以會出現說文章的內容有出入或錯誤,也希望讀者們能及時指出,我會及時加以修正。謝謝,加油!
那麼接下來,就讓我們一起來學習Redis相關的知識點吧!
哦?緩存
什麼是緩存
什麼是緩存呢?緩存就是數據交換的緩衝區(稱作:Cache),這裏我們可以簡單的理解爲存儲在內存中的一個區域或數據。
緩存的作用
那麼緩存究竟有什麼作用呢?緩存的作用,主要有兩個作用:高性能、高併發。
- 高性能
在不使用緩存的情況下,通常查詢所需數據需要從數據庫(如:MySQL)中讀取,讀取結果後我們還可能對結果進行一系列的複雜且耗時的計算,進而得出的我們真正想要的結果。在操作結果不會頻繁發生變化的情況下,使用緩存後,查詢請求一過來,直接從內存查詢獲取到最終的結果,免除了重新進行復雜且耗時的計算,性能極大的提高。 - 高併發
緩存走內存,天然支持高併發(單機甚至可支持到上萬QPS),而相對,數據庫來說,一般併發請求量在2000QPS左右(單機)。
緩存單機承載併發量是MySQL單機的幾十倍。(歸根到底,還是因爲緩存走內存,執行效率高,可以快速查詢到我們想要的結果,立馬返回結束請求)
使用緩存後常見的緩存問題
- 緩存與數據庫雙寫一致性
- 緩存雪崩
- 緩存穿透
- 緩存擊穿
- 緩存併發競爭
(後續會針對這些問題進行分析)
常見的緩存問題分析
緩存雪崩
隨機保存key的過期時間、緩存預熱、緩存高可用架構、緩存持久化方案的實施
緩存穿透
布隆過濾器、保存空值
緩存擊穿
熱點Key問題,能不設置過期時間就不設置、key過期的情況下加鎖控制併發從數據庫中讀取,避免高併發情況下由於緩存過期直接將請求打到數據庫使得數據庫宕機。
緩存與數據庫的雙寫一致性
說起使用緩存,不得不提及這樣一個重要的問題,即緩存和數據庫的數據一致性問題(保證緩存和數據庫保存的數據一致)。
Cache Aside Pattern
介紹
Cache Aside Pattern是一個由國外友人提出的經典的緩存+數據庫的讀寫模式,該模式分爲:
- 讀實踐:
- 先讀緩存,如果緩存中讀取到值,則返回;
- 若緩存中沒有則查詢數據庫,計算值;
- 將計算結果放入緩存中,返回結果。
- 寫實踐:
- 先更新數據庫;
- 再刪除緩存。
進一步分析
針對Cache Aside Pattern的寫實踐我們來進行討論如下幾個問題:
- 問:爲什麼不更新緩存而是刪除緩存?
- “先更新數據庫,再刪除緩存”是否會出現數據不一致問題呢?
- 刪除緩存失敗,緩存內存儲舊數據而數據庫存儲的是新數據,出現數據不一致問題,不滿足業務要求。
- 爲什麼不“先刪除緩存,再更新數據庫”呢?這種情況是否能保證數據一致呢?
- 更新數據庫失敗,但緩存內存儲的數據同數據庫一致,滿足業務要求。
- 但是如果出現併發情況,請求A先刪除緩存,此時有另外個請求B進行讀取,發生緩存未命中,從數據庫讀取舊數據又保存到緩存中,接着請求A繼續更新數據庫。那麼此時同樣會出現緩存同數據庫數據不一致問題。
- …
這裏的數據一致性問題,歸根到底,還是操作的原子性問題(要麼一起成功,要麼一起失敗)。
TODO
總結
針對這個問題,是實際場景中,如果業務上允許緩存與數據庫存在短暫或一段時間的數據不一致,那麼我們儘可能地不去採用“緩存讀寫請求串行化的”等方案去解決數據不一致的問題。
緩存讀寫請求串行化:
- QPS、吞吐量降低
- 讀請求長阻塞
- 多服務實例服務部署的請求路由(將請求打到同一臺機)
- 熱點數據路由,請求傾斜問題
Redis的線程模型
(支持多線程的 Redis 6.0 版本於 2020-05-02 終於發佈,所以在這講的是Redis單線程版的!!!需要注意)
客戶端與Redis的通信流程
這裏先不說一些比較概念性的內容,而是先對《客戶端與Redis的通信流程》結合上圖做一個整體的描述說明,閱讀期間可能會因爲一些專有名詞而產生疑惑甚至出現閱讀障礙,但是不要方,hold住,先看完有個整體大概的印象,知道大概大概後再結合後續具體的說明會進一步瞭解。(當然這裏閱讀之前,你需要對網絡Socket編程有些理解,不懂?沒事,先去了解下再回來繼續看吧。可以瞭解下Java的Socket編程會很有意思哦,寫個Demo,寫個簡單的“聊天室”,對你來說不會是個難事啦~~)
來吧,讓我們來看看這個通信流程是個怎樣的肥事。
- 在Redis啓動初始化,會將Server Socket的
AE_READABLE
事件與連接應答處理器關聯。(我***~~~AE_READABLE
事件、連接應答處理器 咩玩意哦。聽我的,先結合圖儘可能理解先) - 客戶端發起同Redis服務端建立連接時,Server Socket會產生一個
AE_READABLE
事件,IO多路複用程序監聽到該事件後,會將該Socket產生的AE_READABLE
事件壓入隊列中。 - 文件事件分派器會從隊列中取出服務端產生的這個
AE_READABLE
事件會交由連接應答處理器處理。(爲什麼是交給連接應答處理器處理呢?因爲在一開始Server Socket的AE_READABLE
事件與連接應答處理器關聯了哈!) - 連接應答處理器會創建一個同客戶端連接通信的Socket,這裏我們假定該Socket爲 socket01,並將 socket01的
AE_READABLE
事件與命令請求處理器關聯。 - 客戶端發送請求,如:set key value,此時與客戶端連接的socket01會產生
AE_READABLE
事件,同樣地,IO多路複用程序監聽到socket01的事件後,會將socket01產生的AE_READABLE
事件壓入隊列中。 - 文件事件分派器從隊列中取出socket01產生的
AE_READABLE
事件會交由命令請求處理器進行處理。(因爲在第 4 步中已經將 socket01的AE_READABLE
事件與命令請求處理器關聯 了哈) - 命令請求處理器執行客戶端的請求命令,如:在內存中實現對key的set操作,設置爲新的值,接着將socket01的
AE_WRITABLE
事件與命令回覆處理器關聯。 - 客戶端準備好接收請求結果後,Redis中的socket01會產生一個
AE_WRITABLE
事件,同樣會經由IO多路複用程序壓入隊列中。 - 文件事件分派器從隊列中取出socket01產生的
AE_WRITABLE
事件會交由命令回覆處理器進行處理。(因爲在第 7 步中已經將 socket01的AE_WRITABLE
事件與命令回覆處理器關聯 了哈) - 命令回覆處理器會向socket01輸入本次操作的結果,並最後將 socket01的
AE_WRITABLE
事件與命令回覆處理器的關聯解除,最終完成一次與客戶端的通信。
Redis線程模型說明
Redis基於Reactor模型開發了網絡事件處理模型,稱爲文件事件處理器。採用IO多路複用機制,同時監聽多個socket,當socket產生對應事件時,會根據socket的事件來(文件事件分派器)選擇相應的事件處理器來處理事件。
文件事件處理器,包括:多個socket、IO多路複用程序、文件事件分派器、事件處理器。其中事件處理器,包括:連接應答處理器、命令請求處理器、命令回覆處理器。因爲文件事件分派器隊列的消費是單線程的,所以Redis才叫單線程模型。
爲什麼Redis使用的是單線程模型但效率還是很高呢?
- 單線程操作,反倒避免了多線程上下文切換的問題&多線程共享資源的競爭問題。
- 基於IO多路複用機制,非阻塞。
Redis的過期策略&淘汰機制
這一節,讓我們來了解下Redis的過期策略和內存淘汰機制。瞭解這些知識點的原因,在於可以幫助我們清楚地知道Redis針對過期的key是如何進行處理的,解答類似“爲什麼這個key過期了,但還佔用着內存”等問題,也可以讓我們瞭解到在Redis可使用的內存滿了情況下,它又是如何處理的,解答類似“爲什麼我一些沒有設置過期時間的key也被刪除了呢”等問題。
Redis的過期策略
針對過期的key,Redis是如何處理的呢?已過期的key不會立馬被刪除。在默認情況下,Redis每隔100ms會隨機抽取一些設置了過期時間的key,檢查其是否過期,如已過期則進行刪除。爲什麼是隨機抽取一些呢?因爲在定期刪除的情況下,如果設置了過期時間的key真的很多的話,Redis需要全部檢查這些key是否真的過期,那將是一個CPU負載很高的操作。
從上面我們瞭解到Redis的定期刪除可能會遺漏掉一些真的已經過期了的key,那麼這些key又是怎麼被刪除的呢?
Redis的惰性刪除,即在訪問某個key的時候,會先校驗該key是否已過期,如果已過期則進行刪除,並返回空結果。在定期刪除的過程中,如果已經過期的key沒有被抽取到,則在訪問該key的時候將會被刪除。
總結,Redis的過期策略:定期刪除+惰性刪除。
Redis的內存淘汰機制
採用Redis這樣的過期策略,仍會出現很多過期的key沒有被刪除的情況(被定期刪除遺漏,又沒有及時訪問走惰性刪除),堆積在內存中,後續又有新的key存到內存中,直到Redis可使用的內存耗盡,那此時Redis又是如何處理的呢?
答案是:走Redis的內存淘汰機制。
Redis會依據某種策略會一些key進行淘汰,騰出內存空間。其中最常用的策略爲:allkeys-lru,即當內存不足以容納新寫入數據時,在鍵空間中,移除最近最少使用(LRU)的 key。(其他幾種Redis的內存淘汰機制,我就不在這裏做過多說明啦,可參考其他文獻)
Redis使用的是一種近似LRU的算法實現的 allkeys-lru 內存淘汰。LRU算法,參考:緩存淘汰算法–LRU算法。
額外分享:值得了解的是,LRU算法在Java中可以使用 LinkedHashMap 快速實現。
class LRUCache<K, V> extends LinkedHashMap<K, V> {
private final int CACHE_SIZE;
/**
* 傳遞進來最多能緩存多少數據
*
* @param cacheSize 緩存大小
*/
public LRUCache(int cacheSize) {
// true 表示讓 linkedHashMap 按照訪問順序來進行排序,最近訪問的放在頭部,最老訪問的放在尾部。
super((int) Math.ceil(cacheSize / 0.75) + 1, 0.75f, true);
CACHE_SIZE = cacheSize;
}
/**
* 鉤子方法,通過put新增鍵值對的時候,若該方法返回true
* 便移除該map中最老的鍵和值
*/
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
// 當 map中的數據量大於指定的緩存個數的時候,就自動刪除最老的數據。
return size() > CACHE_SIZE;
}
}
Redis持久化
TODO
持久化方式:
RDB
AOF
持久化方式的對比
Redis主從架構
TODO
Redis集羣工作原理等
參考資料
- Java面試——緩存
- 幫你解讀什麼是Redis緩存穿透和緩存雪崩(包含解決方案)
- 緩存穿透、緩存擊穿、緩存雪崩區別和解決方案
- 究竟先操作緩存,還是數據庫?
- 緩存,究竟是淘汰,還是修改?
- 徹底搞懂Redis的線程模型
- 徹底弄懂Redis的內存淘汰策略
- 緩存淘汰算法–LRU算法