Redis 線程模型
客戶端 socket01 向 redis 進程的 server socket 請求建立連接,此時 server socket 會產生一個 AE_READABLE
事件,IO 多路複用程序監聽到 server socket 產生的事件後,將該 socket 壓入隊列中。文件事件分派器從隊列中獲取 socket,交給連接應答處理器。連接應答處理器會創建一個能與客戶端通信的 socket01,並將該 socket01 的 AE_READABLE
事件與命令請求處理器關聯。
假設此時客戶端發送了一個 set key value
請求,此時 redis 中的 socket01 會產生 AE_READABLE
事件,IO 多路複用程序將 socket01 壓入隊列,此時事件分派器從隊列中獲取到 socket01 產生的 AE_READABLE
事件,由於前面 socket01 的 AE_READABLE
事件已經與命令請求處理器關聯,因此事件分派器將事件交給命令請求處理器來處理。命令請求處理器讀取 socket01 的 key value
並在自己內存中完成 key value
的設置。操作完成後,它會將 socket01 的 AE_WRITABLE
事件與命令回覆處理器關聯。
如果此時客戶端準備好接收返回結果了,那麼 redis 中的 socket01 會產生一個 AE_WRITABLE
事件,同樣壓入隊列中,事件分派器找到相關聯的命令回覆處理器,由命令回覆處理器對 socket01 輸入本次操作的一個結果,比如 ok
,之後解除 socket01 的 AE_WRITABLE
事件與命令回覆處理器的關聯。
爲啥 redis 單線程模型也能效率這麼高?
- 純內存操作。
- 核心是基於非阻塞的 IO 多路複用機制。處理事件是純內存操作。
- C 語言實現,一般來說,C 語言實現的程序“距離”操作系統更近,執行速度相對會更快。
- 單線程反而避免了多線程的頻繁上下文切換問題,預防了多線程可能產生的競爭問題。
redis 過期策略
定期刪除+惰性刪除
所謂定期刪除,指的是 redis 默認是每隔 100ms 就隨機抽取一些設置了過期時間的 key,檢查其是否過期,如果過期就刪除。
所謂惰性刪除在你獲取某個 key 的時候,redis 會檢查一下 ,這個 key 如果設置了過期時間那麼是否過期了?如果過期了此時就會刪除,不會給你返回任何東西。
但是這樣:當有大量key過期時 這個策略往往是不夠的。只有走走內存淘汰機制。
這個看具體業務設置適合的淘汰機制
- noeviction: 當內存不足以容納新寫入數據時,新寫入操作會報錯,這個一般沒人用吧,實在是太噁心了。
- allkeys-lru:當內存不足以容納新寫入數據時,在鍵空間中,移除最近最少使用的 key(這個是最常用的)。
- allkeys-random:當內存不足以容納新寫入數據時,在鍵空間中,隨機移除某個 key,這個一般沒人用吧,爲啥要隨機,肯定是把最近最少使用的 key 給幹掉啊。
- volatile-lru:當內存不足以容納新寫入數據時,在設置了過期時間的鍵空間中,移除最近最少使用的 key(這個一般不太合適)。
- volatile-random:當內存不足以容納新寫入數據時,在設置了過期時間的鍵空間中,隨機移除某個 key。
- volatile-ttl:當內存不足以容納新寫入數據時,在設置了過期時間的鍵空間中,有更早過期時間的 key 優先移除。
手寫一個 LRU 算法
利用已有的 JDK 數據結構實現一個 Java 版的 LRU。
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;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
// 當 map中的數據量大於指定的緩存個數的時候,就自動刪除最老的數據。
return size() > CACHE_SIZE;
}
}
Redis 主從架構 與哨兵機制保證高併發 高可用
sentinel,中文名是哨兵。哨兵是 redis 集羣機構中非常重要的一個組件,主要有以下功能:
- 集羣監控:負責監控 redis master 和 slave 進程是否正常工作。
- 消息通知:如果某個 redis 實例有故障,那麼哨兵負責發送消息作爲報警通知給管理員。
- 故障轉移:如果 master node 掛掉了,會自動轉移到 slave node 上。
- 配置中心:如果故障轉移發生了,通知 client 客戶端新的 master 地址。
哨兵用於實現 redis 集羣的高可用,本身也是分佈式的,作爲一個哨兵集羣去運行,互相協同工作。
- 故障轉移時,判斷一個 master node 是否宕機了,需要大部分的哨兵都同意才行,涉及到了分佈式選舉的問題。
- 即使部分哨兵節點掛掉了,哨兵集羣還是能正常工作的,因爲如果一個作爲高可用機制重要組成部分的故障轉移系統本身是單點的,那就很坑爹了。
- 哨兵至少需要 3 個實例,來保證自己的健壯性。
- 哨兵 + redis 主從的部署架構,是不保證數據零丟失的,只能保證 redis 集羣的高可用性。
- 對於哨兵 + redis 主從這種複雜的部署架構,儘量在測試環境和生產環境,都進行充足的測試和演練。
redis 哨兵主備切換的數據丟失問題
異步複製導致的數據丟失:意思就是master還沒來得及同步數據到slave 的時候master 就宕機了。
腦裂,也就是說,某個 master 所在機器突然脫離了正常的網絡,跟其他 slave 機器不能連接,但是實際上 master 還運行着。此時哨兵可能就會認爲 master 宕機了,然後開啓選舉,將其他 slave 切換成了 master。這個時候,集羣裏就會有兩個 master ,也就是所謂的腦裂。
怎麼解決
min-slaves-to-write 1
min-slaves-max-lag 10
表示,要求至少有 1 個 slave,數據複製和同步的延遲不能超過 10 秒。
如果說一旦所有的 slave,數據複製和同步的延遲都超過了 10 秒鐘,那麼這個時候,master 就不會再接收任何請求了。 這是爲了減少數據丟失。
- sdown 是主觀宕機,就一個哨兵如果自己覺得一個 master 宕機了,那麼就是主觀宕機
- odown 是客觀宕機,如果 quorum 數量的哨兵都覺得一個 master 宕機了,那麼就是客觀宕機
redis 持久化的兩種方式
-
RDB:RDB 持久化機制,是對 redis 中的數據執行週期性的持久化。保存的是實際的數據
-
AOF:AOF 機制對每條寫入命令作爲日誌(resp協議命令),以
append-only
的模式寫入一個日誌文件中,在 redis 重啓的時候,可以通過回放 AOF 日誌中的寫入指令來重新構建整個數據集。 -
如果同時使用 RDB 和 AOF 兩種持久化機制,那麼在 redis 重啓的時候,會使用 AOF 來重新構建數據,因爲 AOF 中的數據更加完整。
RDB 優缺點
-
RDB 對 redis 對外提供的讀寫服務,影響非常小,可以讓 redis 保持高性能,因爲 redis 主進程只需要 fork 一個子進程,讓子進程執行磁盤 IO 操作來進行 RDB 持久化即可。
-
由於RDB是保存的數據,恢復就比較快,但是RDB都是每隔一段時間保存一次數據,所以在週期內可能會有數據丟失。
-
RDB 每次在 fork 子進程來執行 RDB 快照數據文件生成的時候,如果數據文件特別大,可能會導致對客戶端提供的服務暫停數毫秒,或者甚至數秒。
AOF優缺點
- AOF 可以更好的保護數據不丟失,一般 AOF 會每隔 1 秒,通過一個後臺線程執行一次
fsync
操作,最多丟失 1 秒鐘的數據。 - AOF 日誌文件以
append-only
模式寫入,所以沒有任何磁盤尋址的開銷,寫入性能非常高,而且文件不容易破損,即使文件尾部破損,也很容易修復。 - 適合做適合做災難性的誤刪除的緊急恢復 比如某人不小心用
flushall
命令清空了所有數據,只要這個時候後臺rewrite
還沒有發生,那麼就可以立即拷貝 AOF 文件,將最後一條flushall
命令給刪了,然後再將該AOF
文件放回去,就可以通過恢復機制,自動恢復所有數據 - 相對來說性能會比RDB低,因爲AOF 一般會配置成每秒
fsync
一次日誌文件。
Redis cluster
分佈式尋址算法
hash 算法(大量緩存重建)
來了一個 key,首先計算 hash 值,然後對節點數取模。然後打在不同的 master 節點上。一旦某一個 master 節點宕機,所有請求過來,都會基於最新的剩餘 master 節點數去取模,嘗試去取數據。這會導致大部分的請求過來,全部無法拿到有效的緩存,導致大量的流量涌入數據庫
一致性 hash 算法+虛擬節點
一致性 hash 算法將整個 hash 值空間組織成一個虛擬的圓環,整個空間按順時針方向組織,下一步將各個 master 節點(使用服務器的 ip 或主機名)進行 hash。這樣就能確定每個節點在其哈希環上的位置。
對key首先計算 hash 值,並確定此數據在環上的位置,從此位置沿環順時針“行走”,遇到的第一個 master 節點就是 key 所在位置。
在一致性哈希算法中,如果一個節點掛了,受影響的數據僅僅是此節點到環空間前一個節點(沿着逆時針方向行走遇到的第一個節點)之間的數據,其它不受影響。增加一個節點也同理。
燃鵝,一致性哈希算法在節點太少時,容易因爲節點分佈不均勻而造成緩存熱點的問題。爲了解決這種熱點問題,一致性 hash 算法引入了虛擬節點機制,即對每一個節點計算多個 hash,每個計算結果位置都放置一個虛擬節點。這樣就實現了數據的均勻分佈,負載均衡。
Redis cluster 的 hash slot 算法
Redis cluster 有固定的 16384
個 hash slot,對每個 key
計算 CRC16
值,然後對 16384
取模,可以獲取 key 對應的 hash slot。
redis cluster 中每個 master 都會持有部分 slot,比如有 3 個 master,那麼可能每個 master 持有 5000 多個 hash slot。hash slot 讓 node 的增加和移除很簡單,增加一個 master,就將其他 master 的 hash slot 移動部分過去,減少一個 master,就將它的 hash slot 移動到其他 master 上去。移動 hash slot 的成本是非常低的。客戶端的 api,可以對指定的數據,讓他們走同一個 hash slot,通過 hash tag
來實現。
任何一臺機器宕機,另外兩個節點,不影響的。因爲 key 找的是 hash slot,不是機器。
緩存雪崩 擊穿 穿透
雪崩
- 事前:redis 高可用,主從+哨兵,redis cluster,避免全盤崩潰。
- 事中:本地 ehcache 緩存 + hystrix 限流&降級,避免 MySQL 被打死。
- 事後:redis 持久化,一旦重啓,自動從磁盤上加載數據,快速恢復緩存數據。
穿透
用戶大量請求庫中不存在的數據
每次系統 從數據庫中只要沒查到,就寫一個空值到緩存裏去,比如 set -999 UNKNOWN
。然後設置一個過期時間,這樣的話,下次有相同的 key 來訪問的時候,在緩存失效之前,都可以直接從緩存中取數據。
擊穿
某個 key 非常熱點,訪問非常頻繁,處於集中式高併發訪問的情況,當這個 key 在失效的瞬間,大量的請求就擊穿了緩存,直接請求數據庫,就像是在一道屏障上鑿開了一個洞
可以將熱點數據設置爲永遠不過期;或者基於 redis or zookeeper 實現互斥鎖,等待第一個請求構建完緩存之後,再釋放鎖,進而其它請求才能通過該 key 訪問數據。