Redis深度歷險-小筆記

應(yīng)用篇

1、Redis分布式鎖

超時問題

如果在加鎖和釋放鎖之間的邏輯執(zhí)行的太長,以至于超出了鎖的超時限制,就會出現(xiàn)問題。因為這時候鎖過期了,第二個線程重新持有了這把鎖,但是緊接著第一個線程執(zhí)行完了業(yè)務(wù)邏輯,就把鎖給釋放了,第三個線程就會在第二個線程邏輯執(zhí)行完之間拿到了鎖。

為了避免這個問題,Redis 分布式鎖不要用于較長時間的任務(wù)。

有一個稍微安全一點的方案是為 set 指令的 value 參數(shù)設(shè)置為一個隨機數(shù),釋放鎖時先匹配隨機數(shù)是否一致,然后再刪除 key,這是為了確保當前線程占有的鎖不會被其它線程釋放,除非這個鎖是過期了被服務(wù)器自動釋放的。

加鎖失敗怎么辦?

客戶端在處理請求時加鎖沒加成功怎么辦。一般有 3 種策略來處理加鎖失?。?/p>

  1. 直接拋出異常,通知用戶稍后重試;
  2. sleep 一會再重試;
  3. 將請求轉(zhuǎn)移至延時隊列,過一會再試;

2、Redis延時隊列

隊列空了怎么辦?

用blpop/brpop替代前面的lpop/rpop, 阻塞讀在隊列沒有數(shù)據(jù)的時候,會立即進入休眠狀態(tài),一旦數(shù)據(jù)到來,則立刻醒過來。消息的延遲幾乎為零。

如何實現(xiàn)延時隊列?

延時隊列可以通過 Redis 的 zset(有序列表) 來實現(xiàn)。我們將消息序列化成一個字符串作為 zset 的value,這個消息的到期處理時間作為score,然后用多個線程輪詢 zset 獲取到期的任務(wù)進行處理,多個線程是為了保障可用性,萬一掛了一個線程還有其它線程可以繼續(xù)處理。因為有多個線程,所以需要考慮并發(fā)爭搶任務(wù),確保任務(wù)不能被多次執(zhí)行。

3、 位圖

使用位圖操作 getbit/setbit 等將 byte 數(shù)組看成「位數(shù)組」來處理。

統(tǒng)計和查找

Redis 提供了位圖統(tǒng)計指令 bitcount 和位圖查找指令 bitpos,bitcount 用來統(tǒng)計指定位置范圍內(nèi) 1 的個數(shù),bitpos 用來查找指定范圍內(nèi)出現(xiàn)的第一個 0 或 1。

比如我們可以通過 bitcount 統(tǒng)計用戶一共簽到了多少天,通過 bitpos 指令查找用戶從哪一天開始第一次簽到。如果指定了范圍參數(shù)[start, end],就可以統(tǒng)計在某個時間范圍內(nèi)用戶簽到了多少天,用戶自某天以后的哪天開始簽到。

4、 HyperLogLog

HyperLogLog 提供不精確的去重計數(shù)方案。

HyperLogLog 提供了兩個指令 pfadd 和 pfcount,一個是增加計數(shù),一個是獲取計數(shù)。

5、 布隆過濾器

布隆過濾器的作用是去重。

布隆過濾器可以理解為一個不怎么精確的 set 結(jié)構(gòu),當你使用它的 contains 方法判斷某個對象是否存在時,它可能會誤判。

布隆過濾器有二個基本指令,bf.add 添加元素,bf.exists 查詢元素是否存在

6、Scan

如何從海量的 key 中找出滿足特定前綴的 key 列表來?

Redis 提供了一個簡單暴力的指令 keys 用來列出所有滿足特定正則字符串規(guī)則的 key。但是有很明顯的兩個缺點。

  1. 沒有 offset、limit 參數(shù),一次性吐出所有滿足條件的 key
  2. keys 算法是遍歷算法,復(fù)雜度是 O(n),如果實例中有千萬級以上的 key,這個指令就會導(dǎo)致 Redis 服務(wù)卡頓。

Redis 為了解決這個問題,它在 2.8 版本中加入了大海撈針的指令——scan

scan 參數(shù)提供了三個參數(shù),第一個是 cursor 整數(shù)值,第二個是 key 的正則模式,第三個是遍歷的 limit hint。第一次遍歷時,cursor 值為 0,然后將返回結(jié)果中第一個整數(shù)值作為下一次遍歷的 cursor。一直遍歷到返回的 cursor 值為 0 時結(jié)束。

scan 遍歷順序

scan 的遍歷順序非常特別。它不是從第一維數(shù)組的第 0 位一直遍歷到末尾,而是采用了高位進位加法來遍歷。之所以使用這樣特殊的方式進行遍歷,是考慮到字典的擴容和縮容時避免槽位的遍歷重復(fù)和遺漏。

大 key 掃描

如果一個 key 太大,那么當它需要擴容時,會一次性申請更大的一塊內(nèi)存,這也會導(dǎo)致卡頓。如果這個大 key 被刪除,內(nèi)存會一次性回收,卡頓現(xiàn)象會再一次產(chǎn)生。要盡量避免大 key 的產(chǎn)生。

如何定位大 key?

為了避免對線上 Redis 帶來卡頓,這就要用到 scan 指令,對于掃描出來的每一個 key,使用 type 指令獲得 key 的類型,然后使用相應(yīng)數(shù)據(jù)結(jié)構(gòu)的 size 或者 len 方法來得到它的大小,對于每一種類型,保留大小的前 N 名作為掃描結(jié)果展示出來。

上面這樣的過程需要編寫腳本,比較繁瑣,不過 Redis 官方已經(jīng)在 redis-cli 指令中提供了這樣的掃描功能,我們可以直接拿來即用。

redis-cli -h 127.0.0.1 -p 7001 –bigkeys

如果你當心這個指令會大幅抬升 Redis 的 ops 導(dǎo)致線上報警,還可以增加一個休眠參數(shù)。

redis-cli -h 127.0.0.1 -p 7001 –bigkeys -i 0.1

上面這個指令每隔 100 條 scan 指令就會休眠 0.1s,ops 就不會劇烈抬升,但是掃描的時間會變長。

7、 GeoHash

GeoHash 算法將二維的經(jīng)緯度數(shù)據(jù)映射到一維的整數(shù),這樣所有的元素都將在掛載到一條線上,距離靠近的二維坐標映射到一維后的點之間距離也會很接近。當我們想要計算「附近的人時」,首先將目標位置映射到這條線上,然后在這個一維的線上獲取附近的點就行了。

8、 漏斗限流

Redis 4.0 提供了一個限流 Redis 模塊,它叫 redis-cell。該模塊也使用了漏斗算法,并提供了原子的限流指令。

 cl.throttle laoqian:reply 15 30 60 1

上面這個指令的意思是允許「用戶」的頻率為每 60s 最多 30 次(漏水速率),漏斗的初始容量為 15,也就是說一開始可以連續(xù)回復(fù) 15 個帖子,然后才開始受漏水速率的影響。

原理

向布隆過濾器中添加 key 時,會使用多個 hash 函數(shù)對 key 進行 hash 算得一個整數(shù)索引值然后對位數(shù)組長度進行取模運算得到一個位置,每個 hash 函數(shù)都會算得一個不同的位置。再把位數(shù)組的這幾個位置都置為 1 就完成了 add 操作。

向布隆過濾器詢問 key 是否存在時,跟 add 一樣,也會把 hash 的幾個位置都算出來,看看位數(shù)組中這幾個位置是否都位 1,只要有一個位為 0,那么說明布隆過濾器中這個 key 不存在。

拓展篇

1、過期策略

redis 會將每個設(shè)置了過期時間的 key 放入到一個獨立的字典中,以后會定時遍歷這個字典來刪除到期的 key。除了定時遍歷之外,它還會使用惰性策略,定時刪除是集中處理,惰性刪除是零散處理。兩種方法結(jié)合去刪除。

Redis 默認會每秒進行十次過期掃描,過期掃描不會遍歷過期字典中所有的 key,而是采用了一種簡單的貪心策略。

  1. 從過期字典中隨機 20 個 key;
  2. 刪除這 20 個 key 中已經(jīng)過期的 key;
  3. 如果過期的 key 比率超過 1/4,那就重復(fù)步驟 1;

Redis 會持續(xù)掃描過期字典 (循環(huán)多次),直到過期字典中過期的 key 變得稀疏,才會停止 (循環(huán)次數(shù)明顯下降)。

2、 再談分布式鎖 - Redlock

如果你很在乎高可用性,希望掛了一臺 redis 完全不受影響,那就應(yīng)該考慮 redlock。

redlock 使用「大多數(shù)機制」。

加鎖時,它會向過半節(jié)點發(fā)送 set(key, value, nx=True, ex=xxx) 指令,只要過半節(jié)點 set 成功,那就認為加鎖成功。釋放鎖時,需要向所有節(jié)點發(fā)送 del 指令。

這些實例之前相互獨立沒有主從關(guān)系,都是平等的。

不過 Redlock 算法還需要考慮出錯重試、時鐘漂移等很多細節(jié)問題,同時因為 Redlock 需要向多個節(jié)點進行讀寫,意味著相比單實例 Redis 性能會下降一些。

3、 Info 指令

時常會遇到很多問題需要診斷,在診斷之前需要了解 Redis 的運行狀態(tài),通過 Info 指令 ,你可以清晰地知道 Redis 內(nèi)部一系列運行參數(shù)。

  1. Redis 每秒執(zhí)行多少次指令?- info stats
  2. Redis 連接了多少客戶端?- info clients
  3. Redis 內(nèi)存占用多大 ? - info memory
  4. 復(fù)制積壓緩沖區(qū)多大?- info replication

復(fù)制積壓緩沖區(qū)大小非常重要,它嚴重影響到主從復(fù)制的效率。當從庫因為網(wǎng)絡(luò)原因臨時斷開了主庫的復(fù)制,然后網(wǎng)絡(luò)恢復(fù)了,又重新連上的時候,這段斷開的時間內(nèi)發(fā)生在 master 上的修改操作指令都會放在積壓緩沖區(qū)中,這樣從庫可以通過積壓緩沖區(qū)恢復(fù)中斷的主從同步過程。

通過查看sync_partial_err變量的次數(shù)來決定是否需要擴大積壓緩沖區(qū),它表示主從半同步復(fù)制失敗的次數(shù)。

4、Stream

Redis5.0 最大的新特性就是多出了一個數(shù)據(jù)結(jié)構(gòu) Stream, 它是支持多播的可持久化的消息隊列。

Stream真的好像kafka啊。。。


Stream 消息太多怎么辦?

在 xadd 的指令提供一個定長長度 maxlen,就可以將老的消息干掉,確保最多不超過指定長度。

消息如果忘記 ACK 會怎樣?

Stream 在每個消費者結(jié)構(gòu)中保存了正在處理中的消息 ID 列表 PEL,如果消費者收到了消息處理完了但是沒有回復(fù) ack,就會導(dǎo)致 PEL 列表不斷增長,如果有很多消費組的話,那么這個 PEL 占用的內(nèi)存就會放大。

PEL 如何避免消息丟失?

在客戶端消費者讀取 Stream 消息時,Redis 服務(wù)器將消息回復(fù)給客戶端的過程中,客戶端突然斷開了連接,消息就丟失了。但是 PEL 里已經(jīng)保存了發(fā)出去的消息 ID。待客戶端重新連上之后,可以再次收到 PEL 中的消息 ID 列表。不過此時 xreadgroup 的起始消息 ID 不能為參數(shù)>,而必須是任意有效的消息 ID,一般將參數(shù)設(shè)為 0-0,表示讀取所有的 PEL 消息以及自last_delivered_id之后的新消息。

如何實現(xiàn)分區(qū) Partition

Redis 的服務(wù)器沒有原生支持分區(qū)能力,如果想要使用分區(qū),那就需要分配多個 Stream,然后在客戶端使用一定的策略來生產(chǎn)消息到不同的 Stream。

集群

1、Cluster

RedisCluster 是 Redis 自己提供的 Redis 集群化方案。 它是去中心化的, 它們之間通過一種特殊的二進制協(xié)議相互交互集群信息。

Redis Cluster 將所有數(shù)據(jù)劃分為 16384 的 slots。

槽位定位算法

Cluster 默認會對 key 值使用 crc32 算法進行 hash 得到一個整數(shù)值,然后用這個整數(shù)值對 16384 進行取模來得到具體槽位。

跳轉(zhuǎn)

當客戶端向一個錯誤的節(jié)點發(fā)出了指令,該節(jié)點會發(fā)現(xiàn)指令的 key 所在的槽位并不歸自己管理,這時它會向客戶端發(fā)送一個特殊的跳轉(zhuǎn)指令攜帶目標操作的節(jié)點地址,告訴客戶端去連這個節(jié)點去獲取數(shù)據(jù)。

遷移

Redis Cluster 提供了工具 redis-trib 可以讓運維人員手動調(diào)整槽位的分配情況。

Redis 遷移的單位是槽,Redis 一個槽一個槽進行遷移,當一個槽正在遷移時,這個槽就處于中間過渡狀態(tài)。這個槽在原節(jié)點的狀態(tài)為migrating,在目標節(jié)點的狀態(tài)為importing,表示數(shù)據(jù)正在從源流向目標。

2、Codis

Codis 使用 Go 語言開發(fā),它是一個代理中間件,它和 Redis 一樣也使用 Redis 協(xié)議對外提供服務(wù),當客戶端向 Codis 發(fā)送指令時,Codis 負責將指令轉(zhuǎn)發(fā)到后面的 Redis 實例來執(zhí)行,并將返回結(jié)果再轉(zhuǎn)回給客戶端。


Codis 將所有的 key 默認劃分為 1024 個槽位(slot),它首先對客戶端傳過來的 key 進行 crc32 運算計算哈希值,再將 hash 后的整數(shù)值對 1024 這個整數(shù)進行取模得到一個余數(shù),這個余數(shù)就是對應(yīng) key 的槽位。

Codis 開始使用 ZooKeeper、 etcd 存儲槽位關(guān)系。Codis 的集群配置中心使用 zk 來實現(xiàn),意味著在部署上增加了 zk 運維的代價


Codis 提供了自動均衡功能

3、Sentinel

Redis Sentinel 集群看成是一個 ZooKeeper 集群,它是集群高可用的心臟,它一般是由 3~5 個節(jié)點組成

它負責持續(xù)監(jiān)控主從節(jié)點的健康,當主節(jié)點掛掉時,自動選擇一個最優(yōu)的從節(jié)點切換為主節(jié)點。


如何解決消息丟失(主從延遲)?

如果主從延遲特別大,那么丟失的數(shù)據(jù)就可能會特別多。Sentinel 無法保證消息完全不丟失,但是也盡可能保證消息少丟失。它有兩個選項可以限制主從延遲過大。

min-slaves-to-write 1
min-slaves-max-lag 10

1、表示主節(jié)點必須至少有一個從節(jié)點在進行正常復(fù)制,否則就停止對外寫服務(wù),喪失可用性。
2、 表示如果 10s 沒有收到從節(jié)點的反饋,就意味著從節(jié)點同步不正常,要么網(wǎng)絡(luò)斷開了,要么一直沒有給反饋。

原理

1、線程 IO 模型

事件輪詢 (多路復(fù)用)

2、通信協(xié)議

RESP 是 Redis 序列化協(xié)議的簡寫。它是一種直觀的文本協(xié)議,優(yōu)勢在于實現(xiàn)異常簡單,解析性能極好。

Redis 協(xié)議將傳輸?shù)慕Y(jié)構(gòu)數(shù)據(jù)分為 5 種最小單元類型,單元結(jié)束時統(tǒng)一加上回車換行符號\r\n

  1. 單行字符串 以 + 符號開頭。
  2. 多行字符串 以 $ 符號開頭,后跟字符串長度。
  3. 整數(shù)值 以 : 符號開頭,后跟整數(shù)的字符串形式。
  4. 錯誤消息 以 - 符號開頭。
  5. 數(shù)組 以 * 號開頭,后跟數(shù)組的長度。

3、 持久化

Redis 的持久化機制有兩種,第一種是快照,第二種是 AOF 日志??煺帐且淮稳總浞荩珹OF 日志是連續(xù)的增量備份。快照是內(nèi)存數(shù)據(jù)的二進制序列化形式,在存儲上非常緊湊,而 AOF 日志記錄的是內(nèi)存數(shù)據(jù)修改的指令記錄文本。AOF 日志在長期的運行過程中會變的無比龐大,數(shù)據(jù)庫重啟時需要加載 AOF 日志進行指令重放,這個時間就會無比漫長。所以需要定期進行 AOF 重寫,給 AOF 日志進行瘦身。


Redis 4.0 混合持久化

重啟 Redis 時,我們很少使用 rdb 來恢復(fù)內(nèi)存狀態(tài),因為會丟失大量數(shù)據(jù)。我們通常使用 AOF 日志重放,但是重放 AOF 日志性能相對 rdb 來說要慢很多,這樣在 Redis 實例很大的情況下,啟動需要花費很長的時間。

Redis 4.0 為了解決這個問題,帶來了一個新的持久化選項——混合持久化。將 rdb 文件的內(nèi)容和增量的 AOF 日志文件存在一起。這里的 AOF 日志不再是全量的日志,而是自持久化開始到持久化結(jié)束的這段時間發(fā)生的增量 AOF 日志,通常這部分 AOF 日志很小


于是在 Redis 重啟的時候,可以先加載 rdb 的內(nèi)容,然后再重放增量 AOF 日志就可以完全替代之前的 AOF 全量文件重放,重啟效率因此大幅得到提升。

4、 管道 (Pipeline)

  • pipeline機制可以優(yōu)化吞吐量,但無法提供原子性/事務(wù)保障,而這個可以通過Redis-Multi等命令實現(xiàn)。
    參考這里
  • 部分讀寫操作存在相關(guān)依賴,無法使用pipeline實現(xiàn),可利用Script機制,但需要在可維護性方面做好取舍。

5、事務(wù)

Redis 禁止在 multi 和 exec 之間執(zhí)行 watch 指令,而必須在 multi 之前做好盯住關(guān)鍵變量,否則會出錯。

Redis 提供了這種 watch 的機制,它就是一種樂觀鎖。

Redis 會檢查關(guān)鍵變量自 watch 之后,是否被修改了 (包括當前事務(wù)所在的客戶端)。如果關(guān)鍵變量被人動過了,exec 指令就會返回 null 回復(fù)告知客戶端事務(wù)執(zhí)行失敗,這個時候客戶端一般會選擇重試。

確切的說,redis這種不算叫事務(wù),因為不能回滾。

6、 PubSub

之前Redis 消息隊列的不足之處,那就是它不支持消息的多播機制。不過新版本Stream支持了。

消息多播

消息多播允許生產(chǎn)者生產(chǎn)一次消息,中間件負責將消息復(fù)制到多個消息隊列,每個消息隊列由相應(yīng)的消費組進行消費。它是分布式系統(tǒng)常用的一種解耦方式,用于將多個消費組的邏輯進行拆分。支持了消息多播,多個消費組的邏輯就可以放到不同的子系統(tǒng)中。

PubSub的缺點

PubSub 的消息是不會持久化的, 正是因為 PubSub 有這些缺點,它幾乎找不到合適的應(yīng)用場景。

7、 小對象壓縮

Redis 如果使用 32bit 進行編譯,內(nèi)部所有數(shù)據(jù)結(jié)構(gòu)所使用的指針空間占用會少一半,如果你對 Redis 使用內(nèi)存不超過 4G,可以考慮使用 32bit 進行編譯,可以節(jié)約大量內(nèi)存。

Redis 的 ziplist 是一個緊湊的字節(jié)數(shù)組結(jié)構(gòu)

8、 主從同步

最終一致

Redis 的主從數(shù)據(jù)是異步同步的,所以分布式的 Redis 系統(tǒng)并不滿足「一致性」要求。當客戶端在 Redis 的主節(jié)點修改了數(shù)據(jù)后,立即返回,即使在主從網(wǎng)絡(luò)斷開的情況下,主節(jié)點依舊可以正常對外提供修改服務(wù),所以 Redis 滿足「可用性」。

Redis 保證「最終一致性」,從節(jié)點會努力追趕主節(jié)點,最終從節(jié)點的狀態(tài)會和主節(jié)點的狀態(tài)將保持一致。如果網(wǎng)絡(luò)斷開了,主從節(jié)點的數(shù)據(jù)將會出現(xiàn)大量不一致,一旦網(wǎng)絡(luò)恢復(fù),從節(jié)點會采用多種策略努力追趕上落后的數(shù)據(jù),繼續(xù)盡力保持和主節(jié)點一致。

源碼

1、 SDS( Simple Dynamic String)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容