Redis:從應用到底層,一文幫你搞定

1、基本類型及底層實現

1.1、String

用途:

適用於簡單 key-value 存儲、setnx key value 實現分佈式鎖、計數器 (原子性)、分佈式全局唯一 ID。

底層:C 語言中 String 用 char[] 數組表示,源碼中用SDS(simple dynamic string) 封裝 char[],這是是 Redis 存儲的最小單元,一個 SDS 最大可以存儲 512M 信息。

struct sdshdr{
  unsigned int len; // 標記char[]的長度
  unsigned int free; //標記char[]中未使用的元素個數
  char buf[]; // 存放元素的坑
}

Redis 對 SDS 再次封裝生成了RedisObject,核心有兩個作用:

  1. 說明是 5 種類型哪一種。

  2. 裏面有指針用來指向 SDS。

當你執行set name sowhat的時候,其實 Redis 會創建兩個 RedisObject 對象,鍵的 RedisObject 和 值的 RedisOjbect 其中它們 type = REDIS_STRING,而 SDS 分別存儲的就是 name 跟 sowhat 字符串咯。

並且 Redis 底層對 SDS 有如下優化:

  1. SDS 修改後大小 > 1M 時 系統會多分配空間來進行空間預分配

  2. SDS 是惰性釋放空間的,你 free 了空間,可是系統把數據記錄下來下次想用時候可直接使用。不用新申請空間。

1.2、List

查看源碼底層 adlist.h 會發現底層就是個 雙端鏈表,該鏈表最大長度爲 2^32-1。常用就這幾個組合。

lpush + lpop = stack 先進後出的棧 

lpush + rpop = queue 先進先出的隊列 

lpush + ltrim = capped collection 有限集合

lpush + brpop = message queue 消息隊列

一般可以用來做簡單的消息隊列,並且當數據量小的時候可能用到獨有的壓縮列表來提升性能。當然專業點還是要 RabbitMQ、ActiveMQ 等

1.3、Hash

散列非常適用於將一些相關的數據存儲在一起,比如用戶的購物車。該類型在日常用途還是挺多的。

這裏需要明確一點:Redis 中只有一個 K,一個 V。其中 K 絕對是字符串對象,而 V 可以是 String、List、Hash、Set、ZSet 任意一種。

hash 的底層主要是採用字典 dict 的結構,整體呈現層層封裝。從小到大如下:

1.3.1、dictEntry

真正的數據節點,包括 key、value 和 next 節點。

1.3.2、dictht

1、數據 dictEntry 類型的數組,每個數組的 item 可能都指向一個鏈表。

2、數組長度 size。

3、sizemask 等於 size - 1。

4、當前 dictEntry 數組中包含總共多少節點。

1.3.3、dict

1、dictType 類型,包括一些自定義函數,這些函數使得 key 和 value 能夠存儲

2、rehashidx 其實是一個標誌量,如果爲-1說明當前沒有擴容,如果不爲 -1 則記錄擴容位置。

3、dictht 數組,兩個 Hash 表。

4、iterators 記錄了當前字典正在進行中的迭代器

組合後結構就是如下

1.3.4、漸進式擴容

爲什麼 dictht ht[2] 是兩個呢?目的是在擴容的同時不影響前端的 CURD,慢慢的把數據從 ht[0] 轉移到 ht[1] 中,同時rehashindex來記錄轉移的情況,當全部轉移完成,將 ht[1] 改成 ht[0] 使用。

rehashidx = -1 說明當前沒有擴容,rehashidx != -1 則表示擴容到數組中的第幾個了。

擴容之後的數組大小爲大於 used*2 的 2 的 n 次方的最小值,跟 HashMap 類似。然後挨個遍歷數組同時調整 rehashidx 的值,對每個 dictEntry[i] 再挨個遍歷鏈表將數據 Hash 後重新映射到 dictht[1] 裏面。並且 dictht[0].usedictht[1].use 是動態變化的。

整個過程的重點在於rehashidx,其爲第一個數組正在移動的下標位置,如果當前內存不夠,或者操作系統繁忙,擴容的過程可以隨時停止。

停止之後如果對該對象進行操作,那是什麼樣子的呢?

1、如果是新增,則直接新增後第二個數組,因爲如果新增到第一個數組,以後還是要移過來,沒必要浪費時間

2、如果是刪除,更新,查詢,則先查找第一個數組,如果沒找到,則再查詢第二個數組。

1.4、Set

如果你明白 Java 中 HashSet 是 HashMap 的簡化版那麼這個 Set 應該也理解了。都是一樣的套路而已。這裏你可以認爲是沒有 Value 的 Dict。看源碼 t.set.c 就可以瞭解本質了。

int setTypeAdd(robj *subject, robj *value) {
    long long llval;
    if (subject->encoding == REDIS_ENCODING_HT) {
         // 看到底層調用的還是dictAdd,只不過第三個參數= NULL
         if (dictAdd(subject->ptr,value,NULL) == DICT_OK) {
            incrRefCount(value);
            return 1;
        }
        ....

1.5、ZSet

範圍查找 的天敵就是 有序集合,看底層 redis.h 後就會發現 Zset 用的就是可以跟二叉樹媲美的跳躍表來實現有序。跳錶就是多層鏈表的結合體,跳錶分爲許多層 (level),每一層都可以看作是數據的索引這些索引的意義就是加快跳錶查找數據速度

每一層的數據都是有序的,上一層數據是下一層數據的子集,並且第一層 (level 1) 包含了全部的數據;層次越高,跳躍性越大,包含的數據越少。並且隨便插入一個數據該數據是否會是跳錶索引完全隨機的跟玩骰子一樣。

跳錶包含一個表頭,它查找數據時,是從上往下,從左往右進行查找。現在找出值爲 37 的節點爲例,來對比說明跳錶和普遍的鏈表。

  1. 沒有跳錶查詢 比如我查詢數據 37,如果沒有上面的索引時候路線如下圖:

  2. 有跳錶查詢 有跳錶查詢 37 的時候路線如下圖:應用場景:

積分排行榜、時間排序新聞、延時隊列。

1.6、Redis Geo

以前寫過 Redis Geo 核心原理解析,想看的直接跳轉即可。他的核心思想就是將地球近似爲球體來看待,然後 GEO 利用 GeoHash 將二維的經緯度轉換成字符串,來實現位置的劃分跟指定距離的查詢。

1.7、HyperLogLog

HyperLogLog :是一種概率數據結構,它使用概率算法來統計集合的近似基數。而它算法的最本源則是伯努利過程 + 分桶 + 調和平均數。具體實現可看  HyperLogLog 講解。

功能:誤差允許範圍內做基數統計 (基數就是指一個集合中不同值的個數) 的時候非常有用,每個 HyperLogLog 的鍵可以計算接近 2^64 不同元素的基數,而大小隻需要 12KB。錯誤率大概在 0.81%。所以如果用做 UV 統計很合適。

HyperLogLog 底層 一共分了 2^14 個桶,也就是 16384 個桶。每個 (registers) 桶中是一個 6 bit 的數組,這裏有個騷操作就是一般人可能直接用一個字節當桶浪費 2 個 bit 空間,但是 Redis 底層只用 6 個然後通過前後拼接實現對內存用到了極致,最終就是 16384*6/8/1024 = 12KB。

1.8、bitmap

BitMap 原本的含義是用一個比特位來映射某個元素的狀態。由於一個比特位只能表示 0 和 1 兩種狀態,所以 BitMap 能映射的狀態有限,但是使用比特位的優勢是能大量的節省內存空間。

在 Redis 中 BitMap 底層是基於字符串類型實現的,可以把 Bitmaps 想象成一個以比特位爲單位的數組,數組的每個單元只能存儲 0 和 1,數組的下標在 Bitmaps 中叫做偏移量,BitMap 的 offset 值上限 2^32 - 1

  1. 用戶簽到

key = 年份:用戶 id  offset = (今天是一年中的第幾天) % (今年的天數)

  1. 統計活躍用戶

使用日期作爲 key,然後用戶 id 爲 offset 設置不同 offset 爲 0 1 即可。

PS : Redis 它的通訊協議是基於 TCP 的應用層協議 RESP(REdis Serialization Protocol)。

1.9、Bloom Filter

使用布隆過濾器得到的判斷結果:不存在的一定不存在,存在的不一定存在

布隆過濾器 原理:

當一個元素被加入集合時,通過 K 個散列函數將這個元素映射成一個位數組中的 K 個點 (有效降低衝突概率),把它們置爲 1。檢索時,我們只要看看這些點是不是都是 1 就知道集合中有沒有它了:如果這些點有任何一個爲 0,則被檢元素一定不在;如果都是 1,則被檢元素很可能在。這就是布隆過濾器的基本思想。

想玩的話可以用 Google 的guava包玩耍一番。

1.10 發佈訂閱

redis 提供了發佈、訂閱模式的消息機制,其中消息訂閱者與發佈者不直接通信,發佈者向指定的頻道(channel)發佈消息,訂閱該頻道的每個客戶端都可以接收到消息。不過比專業的 MQ(RabbitMQ RocketMQ ActiveMQ Kafka) 相比不值一提,這個功能就算球了。

2、持久化

因爲 Redis 數據在內存,斷電既丟,因此持久化到磁盤是必須得有的,Redis 提供了 RDB 跟 AOF 兩種模式。

2.1、RDB

RDB 持久化機制,是對 Redis 中的數據執行週期性的持久化。更適合做冷備。優點:

1、壓縮後的二進制文,適用於備份、全量複製,用於災難恢復加載 RDB 恢復數據遠快於 AOF 方式,適合大規模的數據恢復。

2、如果業務對數據完整性和一致性要求不高,RDB 是很好的選擇。數據恢復比 AOF 快。

缺點:

1、RDB 是週期間隔性的快照文件,數據的完整性和一致性不高,因爲 RDB 可能在最後一次備份時宕機了。

2、備份時佔用內存,因爲 Redis 在備份時會獨立 fork 一個子進程,將數據寫入到一個臨時文件(此時內存中的數據是原來的兩倍哦),最後再將臨時文件替換之前的備份文件。所以要考慮到大概兩倍的數據膨脹性。

注意手動觸發及 COW:

1、SAVE 直接調用 rdbSave ,阻塞 Redis 主進程,導致無法提供服務。2、BGSAVE 則 fork 出一個子進程,子進程負責調用 rdbSave ,在保存完成後向主進程發送信號告知完成。在 BGSAVE 執行期間仍可以繼續處理客戶端的請求

3、Copy On Write 機制,備份的是開始那個時刻內存中的數據,只複製被修改內存頁數據,不是全部內存數據。

4、Copy On Write 時如果父子進程大量寫操作會導致分頁錯誤。

2.2、AOF

AOF 機制對每條寫入命令作爲日誌,以 append-only 的模式寫入一個日誌文件中,因爲這個模式是只追加的方式,所以沒有任何磁盤尋址的開銷,所以很快,有點像 Mysql 中的 binlog。AOF 更適合做熱備。

優點:

AOF 是一秒一次去通過一個後臺的線程 fsync 操作,數據丟失不用怕。

缺點:

1、對於相同數量的數據集而言,AOF 文件通常要大於 RDB 文件。RDB 在恢復大數據集時的速度比 AOF 的恢復速度要快。

2、根據同步策略的不同,AOF 在運行效率上往往會慢於 RDB。總之,每秒同步策略的效率是比較高的。

AOF 整個流程分兩步:第一步是命令的實時寫入,不同級別可能有 1 秒數據損失。命令先追加到aof_buf然後再同步到 AO 磁盤,如果實時寫入磁盤會帶來非常高的磁盤 IO,影響整體性能

第二步是對 aof 文件的重寫,目的是爲了減少 AOF 文件的大小,可以自動觸發或者手動觸發 (BGREWRITEAOF),是 Fork 出子進程操作,期間 Redis 服務仍可用。

1、在重寫期間,由於主進程依然在響應命令,爲了保證最終備份的完整性;它依然會寫入舊的 AOF 中,如果重寫失敗,能夠保證數據不丟失。

2、爲了把重寫期間響應的寫入信息也寫入到新的文件中,因此也會爲子進程保留一個buf,防止新寫的 file 丟失數據。

3、重寫是直接把當前內存的數據生成對應命令,並不需要讀取老的 AOF 文件進行分析、命令合併。

4、無論是 RDB 還是 AOF 都是先寫入一個臨時文件,然後通過rename完成文件的替換工作

關於 Fork 的建議:

1、降低 fork 的頻率,比如可以手動來觸發 RDB 生成快照、與 AOF 重寫;

2、控制 Redis 最大使用內存,防止 fork 耗時過長;

3、配置牛逼點,合理配置 Linux 的內存分配策略,避免因爲物理內存不足導致 fork 失敗。

4、Redis 在執行BGSAVEBGREWRITEAOF命令時,哈希表的負載因子 >=5,而未執行這兩個命令時 >=1。目的是儘量減少寫操作,避免不必要的內存寫入操作。

5、哈希表的擴展因子:哈希表已保存節點數量 / 哈希表大小。因子決定了是否擴展哈希表。

2.3、恢復

啓動時會先檢查 AOF(數據更完整) 文件是否存在,如果不存在就嘗試加載 RDB。

2.4、建議

既然單獨用 RDB 會丟失很多數據。單獨用 AOF,數據恢復沒 RDB 來的快,所以出現問題了第一時間用 RDB 恢復,然後 AOF 做數據補全才說王道。

3、Redis 爲什麼那麼快

3.1、 基於內存實現:

數據都存儲在內存裏,相比磁盤 IO 操作快百倍,操作速率很快。

3.2、高效的數據結構:

Redis 底層多種數據結構支持不同的數據類型,比如 HyperLogLog 它連 2 個字節都不想浪費。

3.3、豐富而合理的編碼:

Redis 底層提供了 豐富而合理的編碼  ,五種數據類型根據長度及元素的個數適配不同的編碼格式。

1、String:自動存儲 int 類型,非 int 類型用 raw 編碼。

2、List:字符串長度且元素個數小於一定範圍使用 ziplist 編碼,否則轉化爲 linkedlist 編碼。

3、Hash:hash 對象保存的鍵值對內的鍵和值字符串長度小於一定值及鍵值對。

4、Set:保存元素爲整數及元素個數小於一定範圍使用 intset 編碼,任意條件不滿足,則使用 hashtable 編碼。

5、Zset:保存的元素個數小於定值且成員長度小於定值使用 ziplist 編碼,任意條件不滿足,則使用 skiplist 編碼。

3.4、合適的線程模型:

I/O 多路複用模型同時監聽客戶端連接,多線程是需要上下文切換的,對於內存數據庫來說這點很致命。

3.5、 Redis6.0 後引入多線程提速:

要知道 讀寫網絡的 read/write 系統耗時 >> Redis 運行執行耗時,Redis 的瓶頸主要在於網絡的 IO 消耗, 優化主要有兩個方向:

1、提高網絡 IO 性能,典型的實現比如使用 DPDK 來替代內核網絡棧的方式 

2、使用多線程充分利用多核,典型的實現比如 Memcached。

協議棧優化的這種方式跟 Redis 關係不大,支持多線程是一種最有效最便捷的操作方式。所以 Redis 支持多線程主要就是兩個原因:

1、可以充分利用服務器 CPU 資源,目前主線程只能利用一個核

2、多線程任務可以分攤 Redis 同步 IO 讀寫負荷

關於多線程須知:

  1. Redis 6.0 版本 默認多線程是關閉的 io-threads-do-reads no

  2. Redis 6.0 版本 開啓多線程後 線程數也要 謹慎設置。

  3. 多線程可以使得性能翻倍,但是多線程只是用來處理網絡數據的讀寫和協議解析,執行命令仍然是單線程順序執行

4、常見問題

4.1、緩存雪崩

雪崩定義:

Redis 中大批量 key 在同一時間同時失效導致所有請求都打到了 MySQL。而 MySQL 扛不住導致大面積崩塌。

雪崩解決方案:

1、緩存數據的過期時間加上個隨機值,防止同一時間大量數據過期現象發生。

2、如果緩存數據庫是分佈式部署,將熱點數據均勻分佈在不同搞得緩存數據庫中。

3、設置熱點數據永遠不過期。

4.2、緩存穿透

穿透定義:

緩存穿透 是 指緩存和數據庫中都沒有的數據,比如 ID 默認 > 0,黑客一直 請求 ID= -12 的數據那麼就會導致數據庫壓力過大,嚴重會擊垮數據庫。

穿透解決方案:

1、後端接口層增加 用戶鑑權校驗參數做校驗等。

2、單個 IP 每秒訪問次數超過閾值直接拉黑 IP,關進小黑屋 1 天,在獲取 IP 代理池的時候我就被拉黑過。

3、從緩存取不到的數據,在數據庫中也沒有取到,這時也可以將 key-value 對寫爲 key-null 失效時間可以爲 15 秒防止惡意攻擊

4、用 Redis 提供的  Bloom Filter 特性也 OK。

4.3、緩存擊穿

擊穿定義:

現象:大併發集中對這一個熱點 key 進行訪問,當這個 Key 在失效的瞬間,持續的大併發就穿破緩存,直接請求數據庫。

擊穿解決:

設置熱點數據永遠不過期 加上互斥鎖也能搞定了

4.4、雙寫一致性

雙寫:緩存數據庫均更新數據,如何保證數據一致性?

1、先更新數據庫,再更新緩存

安全問題:線程 A 更新數據庫 -> 線程 B 更新數據庫 -> 線程 B 更新緩存 -> 線程 A 更新緩存。導致髒讀

業務場景:讀少寫多場景,頻繁更新數據庫而緩存根本沒用。更何況如果緩存是疊加計算後結果更浪費性能

2、先刪緩存,再更新數據庫

A 請求寫來更新緩存。

B 發現緩存不在去數據查詢舊值後寫入緩存。

A 將數據寫入數據庫,此時緩存跟數據庫不一致

因此 FackBook 提出了  Cache Aside Pattern

失效:應用程序先從 cache 取數據,沒有得到,則從數據庫中取數據,成功後,放到緩存中。

命中:應用程序從 cache 中取數據,取到後返回。

更新:先把數據存到數據庫中,成功後,再讓緩存失效

4.5、腦裂

腦裂是指因爲網絡原因,導致 master 節點、slave 節點 和 sentinel 集羣處於不用的網絡分區,此時因爲 sentinel 集羣無法感知到 master 的存在,所以將 slave 節點提升爲 master 節點 此時存在兩個不同的 master 節點就像一個大腦分裂成了兩個。其實在HadoopSpark集羣中都會出現這樣的情況,只是解決方法不同而已 (用 ZK 配合強制殺死)。

集羣腦裂問題中,如果客戶端還在基於原來的 master 節點繼續寫入數據那麼新的 master 節點將無法同步這些數據,當網絡問題解決後 sentinel 集羣將原先的 master 節點降爲 slave 節點,此時再從新的 master 中同步數據將造成大量的數據丟失。

Redis 處理方案是 redis 的配置文件中存在兩個參數

min-replicas-to-write 3  表示連接到master的最少slave數量
min-replicas-max-lag 10  表示slave連接到master的最大延遲時間

如果連接到 master 的 slave 數量 < 第一個參數 且 ping 的延遲時間 <= 第二個參數那麼 master 就會拒絕寫請求,配置了這兩個參數後如果發生了集羣腦裂則原先的 master 節點接收到客戶端的寫入請求會拒絕就可以減少數據同步之後的數據丟失。

4.6、事務

MySQL 中的事務還是挺多道道的還要,而在 Redis 中的事務只要有如下三步:關於事務具體結論:

1、redis 事務就是一次性、順序性、排他性的執行一個隊列中的一系列命令。  

2、Redis 事務沒有隔離級別的概念:批量操作在發送 EXEC 命令前被放入隊列緩存,並不會被實際執行,也就不存在事務內的查詢要看到事務裏的更新,事務外查詢不能看到

3、Redis 不保證原子性:Redis 中單條命令是原子性執行的,但事務不保證原子性。

4、Redis 編譯型錯誤事務中所有代碼均不執行,指令使用錯誤。運行時異常是錯誤命令導致異常,其他命令可正常執行。

5、watch 指令類似於樂觀鎖,在事務提交時,如果 watch 監控的多個 KEY 中任何 KEY 的值已經被其他客戶端更改,則使用 EXEC 執行事務時,事務隊列將不會被執行。

4.7、正確開發步驟

上線前:Redis 高可用,主從 + 哨兵,Redis cluster,避免全盤崩潰。

上線時:本地 ehcache 緩存 + Hystrix 限流 + 降級,避免 MySQL 扛不住。上線後:Redis 持久化採用 RDB + AOF 來保證斷點後自動從磁盤上加載數據,快速恢復緩存數據。

5、分佈式鎖

日常開發中我們可以用 synchronizedLock  實現併發編程。但是 Java 中的鎖只能保證在同一個 JVM 進程內中執行。如果在分佈式集羣環境下用鎖呢?日常一般有兩種選擇方案。

5.1、 Zookeeper 實現分佈式鎖

你需要知道一點基本zookeeper知識:

1、持久節點:客戶端斷開連接 zk 不刪除 persistent 類型節點 2、臨時節點:客戶端斷開連接 zk 刪除 ephemeral 類型節點 3、順序節點:節點後面會自動生成類似 0000001 的數字表示順序 4、節點變化的通知:客戶端註冊了監聽節點變化的時候,會調用回調方法

大致流程如下,其中注意每個節點監控它前面那個節點狀態,從而避免羊羣效應。關於模板代碼百度即可。缺點:

頻繁的創建刪除節點,加上註冊 watch 事件,對於 zookeeper 集羣的壓力比較大,性能也比不上 Redis 實現的分佈式鎖。

5.2、 Redis 實現分佈式鎖

本身原理也比較簡單,Redis 自身就是一個單線程處理器,具備互斥的特性,通過 setNX,exist 等命令就可以完成簡單的分佈式鎖,處理好超時釋放鎖的邏輯即可。

SETNX

SETNX 是 SET if Not eXists 的簡寫,日常指令是SETNX key value,如果 key 不存在則 set 成功返回 1,如果這個 key 已經存在了返回 0。

SETEX

SETEX key seconds value 表達的意思是 將值 value 關聯到 key ,並將 key 的生存時間設爲多少秒。如果 key 已經存在,setex 命令將覆寫舊值。並且 setex 是一個原子性(atomic) 操作。

加鎖:

一般就是用一個標識唯一性的字符串比如 UUID 配合 SETNX 實現加鎖。

解鎖:

這裏用到了 LUA 腳本,LUA 可以保證是原子性的,思路就是判斷一下 Key 和入參是否相等,是的話就刪除,返回成功 1,0 就是失敗。

缺點:

這個鎖是無法重入的,且自己實心的話各種邊邊角角都要考慮到,所以瞭解個大致思路流程即可,工程化還是用開源工具包就行

5.3、 Redisson 實現分佈式鎖

Redisson 是在 Redis 基礎上的一個服務,採用了基於 NIO 的 Netty 框架,不僅能作爲 Redis 底層驅動客戶端,還能將原生的 RedisHash,List,Set,String,Geo,HyperLogLog 等數據結構封裝爲 Java 裏大家最熟悉的映射(Map),列表(List),集(Set),通用對象桶(Object Bucket),地理空間對象桶(Geospatial Bucket),基數估計算法(HyperLogLog)等結構。

這裏我們只是用到了關於分佈式鎖的幾個指令,他的大致底層原理:

Redisson 加鎖解鎖 大致流程圖如下:

6、Redis 過期策略和內存淘汰策略

6.1、Redis 的過期策略

Redis 中 過期策略 通常有以下三種:

1、定時過期

每個設置過期時間的 key 都需要創建一個定時器,到過期時間就會立即對 key 進行清除。該策略可以立即清除過期的數據,對內存很友好;但是會佔用大量的 CPU 資源去處理過期的數據,從而影響緩存的響應時間和吞吐量。

2、惰性過期

只有當訪問一個 key 時,纔會判斷該 key 是否已過期,過期則清除。該策略可以最大化地節省 CPU 資源,卻對內存非常不友好。極端情況可能出現大量的過期 key 沒有再次被訪問,從而不會被清除,佔用大量內存。

3、定期過期

每隔一定的時間,會掃描一定數量的數據庫的 expires 字典中一定數量的 key,並清除其中已過期的 key。該策略是前兩者的一個折中方案。通過調整定時掃描的時間間隔和每次掃描的限定耗時,可以在不同情況下使得 CPU 和內存資源達到最優的平衡效果。

expires 字典會保存所有設置了過期時間的 key 的過期時間數據,其中 key 是指向鍵空間中的某個鍵的指針,value 是該鍵的毫秒精度的 UNIX 時間戳表示的過期時間。鍵空間是指該 Redis 集羣中保存的所有鍵。

Redis 採用的過期策略:惰性刪除 + 定期刪除。memcached 採用的過期策略:惰性刪除

6.2、6 種內存淘汰策略

Redis 的內存淘汰策略是指在 Redis 的用於緩存的內存不足時,怎麼處理需要新寫入且需要申請額外空間的數據。

1、volatile-lru:從已設置過期時間的數據集(server.db[i].expires)中挑選最近最少使用的數據淘汰 

2、volatile-ttl:從已設置過期時間的數據集(server.db[i].expires)中挑選將要過期的數據淘汰 

3、volatile-random:從已設置過期時間的數據集(server.db[i].expires)中任意選擇數據淘汰 

4、allkeys-lru:從數據集(server.db[i].dict)中挑選最近最少使用的數據淘汰 

5、allkeys-random:從數據集(server.db[i].dict)中任意選擇數據淘汰 6、no-enviction(驅逐):禁止驅逐數據,不刪除的意思。

面試常問常考的也就是 LRU 了,大家熟悉的LinkedHashMap中也實現了LRU算法的,實現如下:

class SelfLRUCache<K, V> extends LinkedHashMap<K, V> {
    private final int CACHE_SIZE;
    /**
     * 傳遞進來最多能緩存多少數據
     * @param cacheSize 緩存大小
     */
    public SelfLRUCache(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;
    }
}

6.2、總結

Redis 的內存淘汰策略的選取並不會影響過期的 key 的處理。內存淘汰策略用於處理內存不足時的需要申請額外空間的數據,過期策略用於處理過期的緩存數據

7、Redis 集羣高可用

單機問題有機器故障、容量瓶頸、QPS 瓶頸。在實際應用中,Redis 的多機部署時候會涉及到redis主從複製Sentinel哨兵模式Redis Cluster

bbjoxf

7.1、redis 主從複製

該模式下 具有高可用性且讀寫分離, 會採用 增量同步全量同步 兩種機制。

7.1.1、全量同步

Redis 全量複製一般發生在 Slave 初始化階段,這時 Slave 需要將 Master 上的所有數據都複製一份:

1、slave 連接 master,發送psync命令。

2、master 接收到psync命名後,開始執行 bgsave 命令生成 RDB 文件並使用緩衝區記錄此後執行的所有寫命令。

3、master 發送快照文件到 slave,並在發送期間繼續記錄被執行的寫命令。4、slave 收到快照文件後丟棄所有舊數據,載入收到的快照。

5、master 快照發送完畢後開始向 slave 發送緩衝區中的寫命令。

6、slave 完成對快照的載入,開始接收命令請求,並執行來自 master 緩衝區的寫命令。

7.1.2、增量同步

也叫指令同步,就是從庫重放在主庫中進行的指令。Redis 會把指令存放在一個環形隊列當中,因爲內存容量有限,如果備機一直起不來,不可能把所有的內存都去存指令,也就是說,如果備機一直未同步,指令可能會被覆蓋掉。

Redis 增量複製是指 Slave 初始化後開始正常工作時 master 發生的寫操作同步到 slave 的過程。增量複製的過程主要是 master 每執行一個寫命令就會向 slave 發送相同的寫命令。

7.1.3、Redis 主從同步策略:

1、主從剛剛連接的時候,進行全量同步;全同步結束後,進行增量同步。當然,如果有需要,slave 在任何時候都可以發起全量同步。redis 策略是,無論如何,首先會嘗試進行增量同步,如不成功,要求從機進行全量同步。2、slave 在同步 master 數據時候如果 slave 丟失連接不用怕,slave 在重新連接之後丟失重補

3、一般通過主從來實現讀寫分離,但是如果 master 掛掉後如何保證 Redis 的 HA 呢?引入Sentinel進行 master 的選擇。

7.2、高可用之哨兵模式

Redis-sentinel  本身是一個獨立運行的進程,一般 sentinel 集羣 節點數至少三個且奇數個,它能監控多個 master-slave 集羣,sentinel 節點發現 master 宕機後能進行自動切換。Sentinel 可以監視任意多個主服務器以及主服務器屬下的從服務器,並在被監視的主服務器下線時,自動執行故障轉移操作。這裏需注意sentinel也有single-point-of-failure問題。大致羅列下哨兵用途:

集羣監控:循環監控 master 跟 slave 節點。

消息通知:當它發現有 redis 實例有故障的話,就會發送消息給管理員 

故障轉移:這裏分爲主觀下線 (單獨一個哨兵發現 master 故障了)。客觀下線 (多個哨兵進行抉擇發現達到 quorum 數時候開始進行切換)。

配置中心:如果發生了故障轉移,它會通知將 master 的新地址寫在配置中心告訴客戶端。

7.3、Redis Cluster

RedisCluster 是 Redis 的分佈式解決方案,在 3.0 版本後推出的方案,有效地解決了 Redis 分佈式的需求。

7.3.1、分區規則

常見的分區規則

  1. 節點取餘:hash(key) % N

  2. 一致性哈希:一致性哈希環

  3. 虛擬槽哈希:CRC16[key] & 16383

RedisCluster 採用了虛擬槽分區方式,具題的實現細節如下:

1、採用去中心化的思想,它使用虛擬槽 solt 分區覆蓋到所有節點上,取數據一樣的流程,節點之間使用輕量協議通信 Gossip 來減少帶寬佔用所以性能很高, 

2、自動實現負載均衡與高可用,自動實現 failover 並且支持動態擴展,官方已經玩到可以 1000 個節點 實現的複雜度低。

3、每個 Master 也需要配置主從,並且內部也是採用哨兵模式,如果有半數節點發現某個異常節點會共同決定更改異常節點的狀態。

4、如果集羣中的 master 沒有 slave 節點,則 master 掛掉後整個集羣就會進入 fail 狀態,因爲集羣的 slot 映射不完整。如果集羣超過半數以上的 master 掛掉,集羣都會進入 fail 狀態

5、官方推薦 集羣部署至少要 3 臺以上的 master 節點

8、Redis 限流

經常乘坐北京西二旗地鐵或者在北京西站乘坐的時候經常會遇到一種情況就是如果人很多,地鐵的工作人員拿個小牌前面一檔讓你等會兒再檢票,這就是實際生活應對人流量巨大的措施。

在開發高併發系統時,有三把利器用來保護系統:緩存降級限流。那麼何爲限流呢?顧名思義,限流就是限制流量,就像你寬帶包了 1 個 G 的流量,用完了就沒了。通過限流,我們可以很好地控制系統的 qps,從而達到保護系統的目的。

1、基於 Redis 的 setnx、zset

1.2、setnx

比如我們需要在 10 秒內限定 20 個請求,那麼我們在 setnx 的時候可以設置過期時間 10,當請求的 setnx 數量達到 20 時候即達到了限流效果。

缺點:比如當統計 1-10 秒的時候,無法統計 2-11 秒之內,如果需要統計 N 秒內的 M 個請求,那麼我們的 Redis 中需要保持 N 個 key 等等問題

1.3、zset

其實限流涉及的最主要的就是滑動窗口,上面也提到 1-10 怎麼變成 2-11。其實也就是起始值和末端值都各 + 1 即可。我們可以將請求打造成一個 zset 數組,當每一次請求進來的時候,value 保持唯一,可以用 UUID 生成,而 score 可以用當前時間戳表示,因爲 score 我們可以用來計算當前時間戳之內有多少的請求數量。而 zset 數據結構也提供了 range 方法讓我們可以很輕易的獲取到 2 個時間戳內有多少請求,

缺點:就是 zset 的數據結構會越來越大。

2、漏桶算法

漏桶算法思路:把水比作是請求,漏桶比作是系統處理能力極限,水先進入到漏桶裏,漏桶裏的水按一定速率流出,當流出的速率小於流入的速率時,由於漏桶容量有限,後續進入的水直接溢出(拒絕請求),以此實現限流。

3、令牌桶算法

令牌桶算法的原理:可以理解成醫院的掛號看病,只有拿到號以後纔可以進行診病。

細節流程大致:

1、所有的請求在處理之前都需要拿到一個可用的令牌纔會被處理

2、根據限流大小,設置按照一定的速率往桶裏添加令牌。

3、設置桶最大可容納值,當桶滿時新添加的令牌就被丟棄或者拒絕。

4、請求達到後首先要獲取令牌桶中的令牌,拿着令牌纔可以進行其他的業務邏輯,處理完業務邏輯之後,將令牌直接刪除。

5、令牌桶有最低限額,當桶中的令牌達到最低限額的時候,請求處理完之後將不會刪除令牌,以此保證足夠的限流。

工程化:

1、自定義註解、aop、Redis + Lua 實現限流。

2、推薦 guavaRateLimiter 實現。

9、常見知識點

  1. 字符串模糊查詢時用Keys可能導致線程阻塞,儘量用scan指令進行無阻塞的取出數據然後去重下即可。

  2. 多個操作的情況下記得用pipeLine把所有的命令一次發過去,避免頻繁的發送、接收帶來的網絡開銷,提升性能。

  3. bigkeys 可以掃描 redis 中的大 key,底層是使用 scan 命令去遍歷所有的鍵,對每個鍵根據其類型執行 STRLEN、LLEN、SCARD、HLEN、ZCARD 這些命令獲取其長度或者元素個數。缺陷是線上試用並且個數多不一定空間大,

  4. 線上應用記得開啓 Redis 慢查詢日誌哦,基本思路跟 MySQL 類似。

  5. Redis 中因爲內存分配策略跟增刪數據是會導致內存碎片,你可以重啓服務也可以執行activedefrag yes進行內存重新整理來解決此問題。

1、Ratio >1 表明有內存碎片,越大表明越多嚴重。

2、Ratio < 1 表明正在使用虛擬內存,虛擬內存其實就是硬盤,性能比內存低得多,這是應該增強機器的內存以提高性能。

3、一般來說,mem_fragmentation_ratio 的數值在 1 ~ 1.5 之間是比較健康的。

10、End

關於 Redis 先吹逼這麼多 (本來想寫秒殺的,怕寫太長,估計能看到這就算是認真閱讀者了),如果你感覺沒看夠那得價錢

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/rhKjQukYTod7cWGKYR_Hiw