加入星計(jì)劃,您可以享受以下權(quán)益:

  • 創(chuàng)作內(nèi)容快速變現(xiàn)
  • 行業(yè)影響力擴(kuò)散
  • 作品版權(quán)保護(hù)
  • 300W+ 專業(yè)用戶
  • 1.5W+ 優(yōu)質(zhì)創(chuàng)作者
  • 5000+ 長(zhǎng)期合作伙伴
立即加入
  • 正文
    • 什么是 Redis?
    • Redis 和 Memcached 有什么區(qū)別?
    • 為什么用 Redis 作為 MySQL 的緩存?
    • Redis 數(shù)據(jù)結(jié)構(gòu)
    • Redis 線程模型
    • Redis 持久化
    • Redis 集群
    • Redis 過期刪除與內(nèi)存淘汰
    • Redis 緩存設(shè)計(jì)
    • Redis 實(shí)戰(zhàn)
  • 推薦器件
  • 相關(guān)推薦
  • 電子產(chǎn)業(yè)圖譜
申請(qǐng)入駐 產(chǎn)業(yè)圖譜

Redis 常見面試題(2023 版本):3 萬字 + 40 張圖 |

2023/06/29
2450
加入交流群
掃碼加入
獲取工程師必備禮包
參與熱點(diǎn)資訊討論

大家好,我是小林。

不知不覺《圖解 Redis》系列文章寫了很多了,考慮到一些同學(xué)面試突擊 Redis,所以我整理了 3 萬字 + 40 張圖的 Redis 八股文,共收集了 40 多個(gè)面試題。

發(fā)車!

認(rèn)識(shí) Redis

什么是 Redis?

我們直接看 Redis 官方是怎么介紹自己的。

Redis 官方的介紹原版是英文的,我翻譯成了中文后截圖的,所以有些文字讀起來會(huì)比較拗口,沒關(guān)系,我會(huì)把里面比較重要的特性抽出來講一下。

Redis 是一種基于內(nèi)存的數(shù)據(jù)庫(kù),對(duì)數(shù)據(jù)的讀寫操作都是在內(nèi)存中完成,因此讀寫速度非???/strong>,常用于緩存,消息隊(duì)列、分布式鎖等場(chǎng)景。

Redis 提供了多種數(shù)據(jù)類型來支持不同的業(yè)務(wù)場(chǎng)景,比如 String(字符串)、Hash(哈希)、 List (列表)、Set(集合)、Zset(有序集合)、Bitmaps(位圖)、HyperLogLog(基數(shù)統(tǒng)計(jì))、GEO(地理信息)、Stream(流),并且對(duì)數(shù)據(jù)類型的操作都是原子性的,因?yàn)閳?zhí)行命令由單線程負(fù)責(zé)的,不存在并發(fā)競(jìng)爭(zhēng)的問題。

除此之外,Redis 還支持事務(wù) 、持久化、Lua 腳本、多種集群方案(主從復(fù)制模式、哨兵模式、切片機(jī)群模式)、發(fā)布/訂閱模式,內(nèi)存淘汰機(jī)制、過期刪除機(jī)制等等。

Redis 和 Memcached 有什么區(qū)別?

很多人都說用 Redis 作為緩存,但是 Memcached 也是基于內(nèi)存的數(shù)據(jù)庫(kù),為什么不選擇它作為緩存呢?要解答這個(gè)問題,我們就要弄清楚 Redis 和 Memcached 的區(qū)別。
Redis 與 Memcached 共同點(diǎn)

    都是基于內(nèi)存的數(shù)據(jù)庫(kù),一般都用來當(dāng)做緩存使用。都有過期策略。兩者的性能都非常高。

Redis 與 Memcached 區(qū)別

    Redis 支持的數(shù)據(jù)類型更豐富(String、Hash、List、Set、ZSet),而 Memcached 只支持最簡(jiǎn)單的 key-value 數(shù)據(jù)類型;Redis 支持?jǐn)?shù)據(jù)的持久化,可以將內(nèi)存中的數(shù)據(jù)保持在磁盤中,重啟的時(shí)候可以再次加載進(jìn)行使用,而 Memcached 沒有持久化功能,數(shù)據(jù)全部存在內(nèi)存之中,Memcached 重啟或者掛掉后,數(shù)據(jù)就沒了;Redis 原生支持集群模式,Memcached 沒有原生的集群模式,需要依靠客戶端來實(shí)現(xiàn)往集群中分片寫入數(shù)據(jù);Redis 支持發(fā)布訂閱模型、Lua 腳本、事務(wù)等功能,而 Memcached 不支持;

為什么用 Redis 作為 MySQL 的緩存?

主要是因?yàn)?Redis 具備「高性能」和「高并發(fā)」兩種特性。

1、Redis 具備高性能

假如用戶第一次訪問 MySQL 中的某些數(shù)據(jù)。這個(gè)過程會(huì)比較慢,因?yàn)槭菑挠脖P上讀取的。將該用戶訪問的數(shù)據(jù)緩存在 Redis 中,這樣下一次再訪問這些數(shù)據(jù)的時(shí)候就可以直接從緩存中獲取了,操作 Redis 緩存就是直接操作內(nèi)存,所以速度相當(dāng)快。

如果 MySQL 中的對(duì)應(yīng)數(shù)據(jù)改變的之后,同步改變 Redis 緩存中相應(yīng)的數(shù)據(jù)即可,不過這里會(huì)有 Redis 和 MySQL 雙寫一致性的問題,后面我們會(huì)提到。

2、 Redis 具備高并發(fā)

單臺(tái)設(shè)備的 Redis 的 QPS(Query Per Second,每秒鐘處理完請(qǐng)求的次數(shù)) 是 MySQL 的 10 倍,Redis 單機(jī)的 QPS 能輕松破 10w,而 MySQL 單機(jī)的 QPS 很難破 ?1w。

所以,直接訪問 Redis 能夠承受的請(qǐng)求是遠(yuǎn)遠(yuǎn)大于直接訪問 MySQL 的,所以我們可以考慮把數(shù)據(jù)庫(kù)中的部分?jǐn)?shù)據(jù)轉(zhuǎn)移到緩存中去,這樣用戶的一部分請(qǐng)求會(huì)直接到緩存這里而不用經(jīng)過數(shù)據(jù)庫(kù)。

Redis 數(shù)據(jù)結(jié)構(gòu)

Redis 數(shù)據(jù)類型以及使用場(chǎng)景分別是什么?

Redis 提供了豐富的數(shù)據(jù)類型,常見的有五種數(shù)據(jù)類型:String(字符串),Hash(哈希),List(列表),Set(集合)、Zset(有序集合)

隨著 Redis 版本的更新,后面又支持了四種數(shù)據(jù)類型: BitMap(2.2 版新增)、HyperLogLog(2.8 版新增)、GEO(3.2 版新增)、Stream(5.0 版新增)。
Redis 五種數(shù)據(jù)類型的應(yīng)用場(chǎng)景:

    String 類型的應(yīng)用場(chǎng)景:緩存對(duì)象、常規(guī)計(jì)數(shù)、分布式鎖、共享 session 信息等。List 類型的應(yīng)用場(chǎng)景:消息隊(duì)列(但是有兩個(gè)問題:1. 生產(chǎn)者需要自行實(shí)現(xiàn)全局唯一 ID;2. 不能以消費(fèi)組形式消費(fèi)數(shù)據(jù))等。Hash 類型:緩存對(duì)象、購(gòu)物車等。Set 類型:聚合計(jì)算(并集、交集、差集)場(chǎng)景,比如點(diǎn)贊、共同關(guān)注、抽獎(jiǎng)活動(dòng)等。Zset 類型:排序場(chǎng)景,比如排行榜、電話和姓名排序等。

Redis 后續(xù)版本又支持四種數(shù)據(jù)類型,它們的應(yīng)用場(chǎng)景如下:

    BitMap(2.2 版新增):二值狀態(tài)統(tǒng)計(jì)的場(chǎng)景,比如簽到、判斷用戶登陸狀態(tài)、連續(xù)簽到用戶總數(shù)等;HyperLogLog(2.8 版新增):海量數(shù)據(jù)基數(shù)統(tǒng)計(jì)的場(chǎng)景,比如百萬級(jí)網(wǎng)頁(yè) UV 計(jì)數(shù)等;GEO(3.2 版新增):存儲(chǔ)地理位置信息的場(chǎng)景,比如滴滴叫車;Stream(5.0 版新增):消息隊(duì)列,相比于基于 List 類型實(shí)現(xiàn)的消息隊(duì)列,有這兩個(gè)特有的特性:自動(dòng)生成全局唯一消息ID,支持以消費(fèi)組形式消費(fèi)數(shù)據(jù)。

::: tip

想深入了解這 9 種數(shù)據(jù)類型,可以看這篇:2萬字 + 20 張圖 | 細(xì)說 Redis 常見數(shù)據(jù)類型和應(yīng)用場(chǎng)景

五種常見的 Redis 數(shù)據(jù)類型是怎么實(shí)現(xiàn)?

我畫了一張 Redis 數(shù)據(jù)類型和底層數(shù)據(jù)結(jié)構(gòu)的對(duì)應(yīng)關(guān)圖,左邊是 Redis 3.0版本的,也就是《Redis 設(shè)計(jì)與實(shí)現(xiàn)》這本書講解的版本,現(xiàn)在看還是有點(diǎn)過時(shí)了,右邊是現(xiàn)在 Redis 7.0 版本的。

String 類型內(nèi)部實(shí)現(xiàn)

String 類型的底層的數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)主要是 SDS(簡(jiǎn)單動(dòng)態(tài)字符串)。
SDS 和我們認(rèn)識(shí)的 C 字符串不太一樣,之所以沒有使用 C 語(yǔ)言的字符串表示,因?yàn)?SDS 相比于 C 的原生字符串:

SDS 不僅可以保存文本數(shù)據(jù),還可以保存二進(jìn)制數(shù)據(jù)

    • 。因?yàn)?SDS 使用 len 屬性的值而不是空字符來判斷字符串是否結(jié)束,并且 SDS 的所有 API 都會(huì)以處理二進(jìn)制的方式來處理 SDS 存放在 buf[] 數(shù)組里的數(shù)據(jù)。所以 SDS 不光能存放文本數(shù)據(jù),而且能保存圖片、音頻、視頻、壓縮文件這樣的二進(jìn)制數(shù)據(jù)。**SDS 獲取字符串長(zhǎng)度的時(shí)間復(fù)雜度是 O(1)**。因?yàn)?C 語(yǔ)言的字符串并不記錄自身長(zhǎng)度,所以獲取長(zhǎng)度的復(fù)雜度為 O(n);而 SDS 結(jié)構(gòu)里用 len 屬性記錄了字符串長(zhǎng)度,所以復(fù)雜度為 O(1)。

Redis 的 SDS API 是安全的,拼接字符串不會(huì)造成緩沖區(qū)溢出

    。因?yàn)?SDS 在拼接字符串之前會(huì)檢查 SDS 空間是否滿足要求,如果空間不夠會(huì)自動(dòng)擴(kuò)容,所以不會(huì)導(dǎo)致緩沖區(qū)溢出的問題。

List 類型內(nèi)部實(shí)現(xiàn)

List 類型的底層數(shù)據(jù)結(jié)構(gòu)是由雙向鏈表或壓縮列表實(shí)現(xiàn)的:

    • 如果列表的元素個(gè)數(shù)小于 512 個(gè)(默認(rèn)值,可由 list-max-ziplist-entries 配置),列表每個(gè)元素的值都小于 64 字節(jié)(默認(rèn)值,可由 list-max-ziplist-value 配置),Redis 會(huì)使用

壓縮列表

    • 作為 List 類型的底層數(shù)據(jù)結(jié)構(gòu);如果列表的元素不滿足上面的條件,Redis 會(huì)使用

雙向鏈表

    作為 List 類型的底層數(shù)據(jù)結(jié)構(gòu);

但是在 Redis 3.2 版本之后,List 數(shù)據(jù)類型底層數(shù)據(jù)結(jié)構(gòu)就只由 quicklist 實(shí)現(xiàn)了,替代了雙向鏈表和壓縮列表。

Hash 類型內(nèi)部實(shí)現(xiàn)

Hash 類型的底層數(shù)據(jù)結(jié)構(gòu)是由壓縮列表或哈希表實(shí)現(xiàn)的:

    • 如果哈希類型元素個(gè)數(shù)小于 512 個(gè)(默認(rèn)值,可由 hash-max-ziplist-entries 配置),所有值小于 64 字節(jié)(默認(rèn)值,可由 hash-max-ziplist-value 配置)的話,Redis 會(huì)使用

壓縮列表

    • 作為 Hash 類型的底層數(shù)據(jù)結(jié)構(gòu);如果哈希類型元素不滿足上面條件,Redis 會(huì)使用

哈希表

    作為 Hash 類型的底層數(shù)據(jù)結(jié)構(gòu)。

在 Redis 7.0 中,壓縮列表數(shù)據(jù)結(jié)構(gòu)已經(jīng)廢棄了,交由 listpack 數(shù)據(jù)結(jié)構(gòu)來實(shí)現(xiàn)了。

Set 類型內(nèi)部實(shí)現(xiàn)

Set 類型的底層數(shù)據(jù)結(jié)構(gòu)是由哈希表或整數(shù)集合實(shí)現(xiàn)的:

    • 如果集合中的元素都是整數(shù)且元素個(gè)數(shù)小于 512 (默認(rèn)值,set-maxintset-entries配置)個(gè),Redis 會(huì)使用

整數(shù)集合

    • 作為 Set 類型的底層數(shù)據(jù)結(jié)構(gòu);如果集合中的元素不滿足上面條件,則 Redis 使用

哈希表

    作為 Set 類型的底層數(shù)據(jù)結(jié)構(gòu)。

ZSet 類型內(nèi)部實(shí)現(xiàn)

Zset 類型的底層數(shù)據(jù)結(jié)構(gòu)是由壓縮列表或跳表實(shí)現(xiàn)的:

    • 如果有序集合的元素個(gè)數(shù)小于 128 個(gè),并且每個(gè)元素的值小于 64 字節(jié)時(shí),Redis 會(huì)使用

壓縮列表

    • 作為 Zset 類型的底層數(shù)據(jù)結(jié)構(gòu);如果有序集合的元素不滿足上面的條件,Redis 會(huì)使用

跳表

    作為 Zset 類型的底層數(shù)據(jù)結(jié)構(gòu);

在 Redis 7.0 中,壓縮列表數(shù)據(jù)結(jié)構(gòu)已經(jīng)廢棄了,交由 listpack 數(shù)據(jù)結(jié)構(gòu)來實(shí)現(xiàn)了。

::: tip

想深入了解這 9 種數(shù)據(jù)結(jié)構(gòu),可以看這篇:2萬字 + 40 張圖 | 細(xì)說 Redis 數(shù)據(jù)結(jié)構(gòu)

Redis 線程模型

Redis 是單線程嗎?

Redis 單線程指的是「接收客戶端請(qǐng)求->解析請(qǐng)求 ->進(jìn)行數(shù)據(jù)讀寫等操作->發(fā)送數(shù)據(jù)給客戶端」這個(gè)過程是由一個(gè)線程(主線程)來完成的,這也是我們常說 Redis 是單線程的原因。

但是,Redis 程序并不是單線程的,Redis 在啟動(dòng)的時(shí)候,是會(huì)啟動(dòng)后臺(tái)線程(BIO)的:

Redis 在 2.6 版本

    • ,會(huì)啟動(dòng) 2 個(gè)后臺(tái)線程,分別處理關(guān)閉文件、AOF 刷盤這兩個(gè)任務(wù);

Redis 在 4.0 版本之后

    ,新增了一個(gè)新的后臺(tái)線程,用來異步釋放 Redis 內(nèi)存,也就是 lazyfree 線程。例如執(zhí)行 unlink key / flushdb async / flushall async 等命令,會(huì)把這些刪除操作交給后臺(tái)線程來執(zhí)行,好處是不會(huì)導(dǎo)致 Redis 主線程卡頓。因此,當(dāng)我們要?jiǎng)h除一個(gè)大 key 的時(shí)候,不要使用 del 命令刪除,因?yàn)?del 是在主線程處理的,這樣會(huì)導(dǎo)致 Redis 主線程卡頓,因此我們應(yīng)該使用 unlink 命令來異步刪除大key。

之所以 Redis 為「關(guān)閉文件、AOF 刷盤、釋放內(nèi)存」這些任務(wù)創(chuàng)建單獨(dú)的線程來處理,是因?yàn)檫@些任務(wù)的操作都是很耗時(shí)的,如果把這些任務(wù)都放在主線程來處理,那么 Redis 主線程就很容易發(fā)生阻塞,這樣就無法處理后續(xù)的請(qǐng)求了。

后臺(tái)線程相當(dāng)于一個(gè)消費(fèi)者,生產(chǎn)者把耗時(shí)任務(wù)丟到任務(wù)隊(duì)列中,消費(fèi)者(BIO)不停輪詢這個(gè)隊(duì)列,拿出任務(wù)就去執(zhí)行對(duì)應(yīng)的方法即可。

關(guān)閉文件、AOF 刷盤、釋放內(nèi)存這三個(gè)任務(wù)都有各自的任務(wù)隊(duì)列:

    BIO_CLOSE_FILE,關(guān)閉文件任務(wù)隊(duì)列:當(dāng)隊(duì)列有任務(wù)后,后臺(tái)線程會(huì)調(diào)用 close(fd) ,將文件關(guān)閉;BIO_AOF_FSYNC,AOF刷盤任務(wù)隊(duì)列:當(dāng) AOF 日志配置成 everysec 選項(xiàng)后,主線程會(huì)把 AOF 寫日志操作封裝成一個(gè)任務(wù),也放到隊(duì)列中。當(dāng)發(fā)現(xiàn)隊(duì)列有任務(wù)后,后臺(tái)線程會(huì)調(diào)用 fsync(fd),將 AOF 文件刷盤,BIO_LAZY_FREE,lazy free 任務(wù)隊(duì)列:當(dāng)隊(duì)列有任務(wù)后,后臺(tái)線程會(huì) free(obj) 釋放對(duì)象 / free(dict) 刪除數(shù)據(jù)庫(kù)所有對(duì)象 / free(skiplist) 釋放跳表對(duì)象;
Redis 單線程模式是怎樣的?

Redis 6.0 版本之前的單線模式如下圖:

圖中的藍(lán)色部分是一個(gè)事件循環(huán),是由主線程負(fù)責(zé)的,可以看到網(wǎng)絡(luò) I/O 和命令處理都是單線程。
Redis 初始化的時(shí)候,會(huì)做下面這幾件事情:

    首先,調(diào)用 epoll_create() 創(chuàng)建一個(gè) epoll 對(duì)象和調(diào)用 socket() 創(chuàng)建一個(gè)服務(wù)端 socket然后,調(diào)用 bind() 綁定端口和調(diào)用 listen() 監(jiān)聽該 socket;然后,將調(diào)用 epoll_ctl() 將 listen socket 加入到 epoll,同時(shí)注冊(cè)「連接事件」處理函數(shù)。

初始化完后,主線程就進(jìn)入到一個(gè)事件循環(huán)函數(shù),主要會(huì)做以下事情:

    • 首先,先調(diào)用

處理發(fā)送隊(duì)列函數(shù)

    • ,看是發(fā)送隊(duì)列里是否有任務(wù),如果有發(fā)送任務(wù),則通過 write 函數(shù)將客戶端發(fā)送緩存區(qū)里的數(shù)據(jù)發(fā)送出去,如果這一輪數(shù)據(jù)沒有發(fā)送完,就會(huì)注冊(cè)寫事件處理函數(shù),等待 epoll_wait 發(fā)現(xiàn)可寫后再處理 。接著,調(diào)用 epoll_wait 函數(shù)等待事件的到來:

      • 如果是

連接事件

      • 到來,則會(huì)調(diào)用

連接事件處理函數(shù)

      • ,該函數(shù)會(huì)做這些事情:調(diào)用 accpet 獲取已連接的 socket -> ?調(diào)用 epoll_ctl 將已連接的 socket 加入到 epoll -> 注冊(cè)「讀事件」處理函數(shù);如果是

讀事件

      • 到來,則會(huì)調(diào)用

讀事件處理函數(shù)

      • ,該函數(shù)會(huì)做這些事情:調(diào)用 read 獲取客戶端發(fā)送的數(shù)據(jù) -> 解析命令 -> 處理命令 -> 將客戶端對(duì)象添加到發(fā)送隊(duì)列 -> 將執(zhí)行結(jié)果寫到發(fā)送緩存區(qū)等待發(fā)送;如果是

寫事件

      • 到來,則會(huì)調(diào)用

寫事件處理函數(shù)

    • ,該函數(shù)會(huì)做這些事情:通過 write 函數(shù)將客戶端發(fā)送緩存區(qū)里的數(shù)據(jù)發(fā)送出去,如果這一輪數(shù)據(jù)沒有發(fā)送完,就會(huì)繼續(xù)注冊(cè)寫事件處理函數(shù),等待 epoll_wait 發(fā)現(xiàn)可寫后再處理 。

以上就是 Redis 單線模式的工作方式,如果你想看源碼解析,可以參考這一篇:為什么單線程的 Redis 如何做到每秒數(shù)萬 QPS ?

Redis 采用單線程為什么還這么快?

官方使用基準(zhǔn)測(cè)試的結(jié)果是,單線程的 Redis 吞吐量可以達(dá)到 10W/每秒,如下圖所示:

之所以 Redis 采用單線程(網(wǎng)絡(luò) I/O 和執(zhí)行命令)那么快,有如下幾個(gè)原因:

    • Redis 的大部分操作

都在內(nèi)存中完成

    • ,并且采用了高效的數(shù)據(jù)結(jié)構(gòu),因此 Redis 瓶頸可能是機(jī)器的內(nèi)存或者網(wǎng)絡(luò)帶寬,而并非 CPU,既然 CPU 不是瓶頸,那么自然就采用單線程的解決方案了;Redis 采用單線程模型可以

避免了多線程之間的競(jìng)爭(zhēng)

    • ,省去了多線程切換帶來的時(shí)間和性能上的開銷,而且也不會(huì)導(dǎo)致死鎖問題。Redis 采用了

I/O 多路復(fù)用機(jī)制

    處理大量的客戶端 Socket 請(qǐng)求,IO 多路復(fù)用機(jī)制是指一個(gè)線程處理多個(gè) IO 流,就是我們經(jīng)常聽到的 select/epoll 機(jī)制。簡(jiǎn)單來說,在 Redis 只運(yùn)行單線程的情況下,該機(jī)制允許內(nèi)核中,同時(shí)存在多個(gè)監(jiān)聽 Socket 和已連接 Socket。內(nèi)核會(huì)一直監(jiān)聽這些 Socket 上的連接請(qǐng)求或數(shù)據(jù)請(qǐng)求。一旦有請(qǐng)求到達(dá),就會(huì)交給 Redis 線程處理,這就實(shí)現(xiàn)了一個(gè) Redis 線程處理多個(gè) IO 流的效果。
Redis 6.0 之前為什么使用單線程?

我們都知道單線程的程序是無法利用服務(wù)器的多核 CPU 的,那么早期 Redis 版本的主要工作(網(wǎng)絡(luò) I/O 和執(zhí)行命令)為什么還要使用單線程呢?我們不妨先看一下Redis官方給出的FAQ。

核心意思是:CPU 并不是制約 Redis 性能表現(xiàn)的瓶頸所在,更多情況下是受到內(nèi)存大小和網(wǎng)絡(luò)I/O的限制,所以 Redis 核心網(wǎng)絡(luò)模型使用單線程并沒有什么問題,如果你想要使用服務(wù)的多核CPU,可以在一臺(tái)服務(wù)器上啟動(dòng)多個(gè)節(jié)點(diǎn)或者采用分片集群的方式。

除了上面的官方回答,選擇單線程的原因也有下面的考慮。

使用了單線程后,可維護(hù)性高,多線程模型雖然在某些方面表現(xiàn)優(yōu)異,但是它卻引入了程序執(zhí)行順序的不確定性,帶來了并發(fā)讀寫的一系列問題,增加了系統(tǒng)復(fù)雜度、同時(shí)可能存在線程切換、甚至加鎖解鎖、死鎖造成的性能損耗。

Redis 6.0 之后為什么引入了多線程?

雖然 Redis 的主要工作(網(wǎng)絡(luò) I/O 和執(zhí)行命令)一直是單線程模型,但是在 Redis 6.0 版本之后,也采用了多個(gè) I/O 線程來處理網(wǎng)絡(luò)請(qǐng)求,這是因?yàn)殡S著網(wǎng)絡(luò)硬件的性能提升,Redis 的性能瓶頸有時(shí)會(huì)出現(xiàn)在網(wǎng)絡(luò) I/O 的處理上。

所以為了提高網(wǎng)絡(luò) I/O 的并行度,Redis 6.0 對(duì)于網(wǎng)絡(luò) I/O 采用多線程來處理。但是對(duì)于命令的執(zhí)行,Redis 仍然使用單線程來處理,所以大家不要誤解 Redis 有多線程同時(shí)執(zhí)行命令。

Redis 官方表示,Redis 6.0 版本引入的多線程 I/O 特性對(duì)性能提升至少是一倍以上

Redis 6.0 版本支持的 I/O ?多線程特性,默認(rèn)情況下 I/O 多線程只針對(duì)發(fā)送響應(yīng)數(shù)據(jù)(write client socket),并不會(huì)以多線程的方式處理讀請(qǐng)求(read client socket)。要想開啟多線程處理客戶端讀請(qǐng)求,就需要把 ?Redis.conf ?配置文件中的 io-threads-do-reads 配置項(xiàng)設(shè)為 yes。

//讀請(qǐng)求也使用io多線程
io-threads-do-reads?yes?

同時(shí), Redis.conf ?配置文件中提供了 ?IO 多線程個(gè)數(shù)的配置項(xiàng)。

//?io-threads?N,表示啟用?N-1?個(gè)?I/O?多線程(主線程也算一個(gè)?I/O?線程)
io-threads?4?

關(guān)于線程數(shù)的設(shè)置,官方的建議是如果為 4 核的 CPU,建議線程數(shù)設(shè)置為 2 或 3,如果為 8 核 CPU 建議線程數(shù)設(shè)置為 6,線程數(shù)一定要小于機(jī)器核數(shù),線程數(shù)并不是越大越好。

因此, Redis 6.0 版本之后,Redis 在啟動(dòng)的時(shí)候,默認(rèn)情況下會(huì)額外創(chuàng)建 6 個(gè)線程這里的線程數(shù)不包括主線程):

    Redis-server : Redis的主線程,主要負(fù)責(zé)執(zhí)行命令;bio_close_file、bio_aof_fsync、bio_lazy_free:三個(gè)后臺(tái)線程,分別異步處理關(guān)閉文件任務(wù)、AOF刷盤任務(wù)、釋放內(nèi)存任務(wù);io_thd_1、io_thd_2、io_thd_3:三個(gè) I/O 線程,io-threads 默認(rèn)是 4 ,所以會(huì)啟動(dòng) 3(4-1)個(gè) I/O 多線程,用來分擔(dān) Redis 網(wǎng)絡(luò) I/O 的壓力。

Redis 持久化

Redis 如何實(shí)現(xiàn)數(shù)據(jù)不丟失?

Redis 的讀寫操作都是在內(nèi)存中,所以 Redis 性能才會(huì)高,但是當(dāng) Redis 重啟后,內(nèi)存中的數(shù)據(jù)就會(huì)丟失,那為了保證內(nèi)存中的數(shù)據(jù)不會(huì)丟失,Redis 實(shí)現(xiàn)了數(shù)據(jù)持久化的機(jī)制,這個(gè)機(jī)制會(huì)把數(shù)據(jù)存儲(chǔ)到磁盤,這樣在 ?Redis 重啟就能夠從磁盤中恢復(fù)原有的數(shù)據(jù)。

Redis 共有三種數(shù)據(jù)持久化的方式:

AOF 日志

    • :每執(zhí)行一條寫操作命令,就把該命令以追加的方式寫入到一個(gè)文件里;

RDB 快照

    • :將某一時(shí)刻的內(nèi)存數(shù)據(jù),以二進(jìn)制的方式寫入磁盤;

混合持久化方式

    :Redis ?4.0 新增的方式,集成了 AOF 和 RBD 的優(yōu)點(diǎn);
AOF 日志是如何實(shí)現(xiàn)的?

Redis 在執(zhí)行完一條寫操作命令后,就會(huì)把該命令以追加的方式寫入到一個(gè)文件里,然后 Redis 重啟時(shí),會(huì)讀取該文件記錄的命令,然后逐一執(zhí)行命令的方式來進(jìn)行數(shù)據(jù)恢復(fù)。

我這里以「_set name xiaolin_」命令作為例子,Redis 執(zhí)行了這條命令后,記錄在 AOF 日志里的內(nèi)容如下圖:

我這里給大家解釋下。

「*3」表示當(dāng)前命令有三個(gè)部分,每部分都是以「數(shù)字」開頭,后面緊跟著具體的命令、鍵或值。然后,這里的「數(shù)字」表示這部分中的命令、鍵或值一共有多少字節(jié)。例如,「3 set」表示這部分有 3 個(gè)字節(jié),也就是「set」命令這個(gè)字符串的長(zhǎng)度。

為什么先執(zhí)行命令,再把數(shù)據(jù)寫入日志呢?

Reids 是先執(zhí)行寫操作命令后,才將該命令記錄到 AOF 日志里的,這么做其實(shí)有兩個(gè)好處。

避免額外的檢查開銷

    • :因?yàn)槿绻葘懖僮髅钣涗浀?AOF 日志里,再執(zhí)行該命令的話,如果當(dāng)前的命令語(yǔ)法有問題,那么如果不進(jìn)行命令語(yǔ)法檢查,該錯(cuò)誤的命令記錄到 AOF 日志里后,Redis 在使用日志恢復(fù)數(shù)據(jù)時(shí),就可能會(huì)出錯(cuò)。

不會(huì)阻塞當(dāng)前寫操作命令的執(zhí)行

    :因?yàn)楫?dāng)寫操作命令執(zhí)行成功后,才會(huì)將命令記錄到 AOF 日志。

當(dāng)然,這樣做也會(huì)帶來風(fēng)險(xiǎn):

數(shù)據(jù)可能會(huì)丟失:

    • 執(zhí)行寫操作命令和記錄日志是兩個(gè)過程,那當(dāng) Redis 在還沒來得及將命令寫入到硬盤時(shí),服務(wù)器發(fā)生宕機(jī)了,這個(gè)數(shù)據(jù)就會(huì)有丟失的風(fēng)險(xiǎn)。

可能阻塞其他操作:

    由于寫操作命令執(zhí)行成功后才記錄到 AOF 日志,所以不會(huì)阻塞當(dāng)前命令的執(zhí)行,但因?yàn)?AOF 日志也是在主線程中執(zhí)行,所以當(dāng) Redis 把日志文件寫入磁盤的時(shí)候,還是會(huì)阻塞后續(xù)的操作無法執(zhí)行。

AOF 寫回策略有幾種?

先來看看,Redis 寫入 AOF 日志的過程,如下圖:

具體說說:

    Redis 執(zhí)行完寫操作命令后,會(huì)將命令追加到 server.aof_buf 緩沖區(qū);然后通過 write() 系統(tǒng)調(diào)用,將 aof_buf 緩沖區(qū)的數(shù)據(jù)寫入到 AOF 文件,此時(shí)數(shù)據(jù)并沒有寫入到硬盤,而是拷貝到了內(nèi)核緩沖區(qū) page cache,等待內(nèi)核將數(shù)據(jù)寫入硬盤;具體內(nèi)核緩沖區(qū)的數(shù)據(jù)什么時(shí)候?qū)懭氲接脖P,由內(nèi)核決定。

Redis 提供了 3 種寫回硬盤的策略,控制的就是上面說的第三步的過程。
在 Redis.conf 配置文件中的 appendfsync 配置項(xiàng)可以有以下 3 種參數(shù)可填:

Always,這個(gè)單詞的意思是「總是」,所以它的意思是每次寫操作命令執(zhí)行完后,同步將 AOF 日志數(shù)據(jù)寫回硬盤;

Everysec,這個(gè)單詞的意思是「每秒」,所以它的意思是每次寫操作命令執(zhí)行完后,先將命令寫入到 AOF 文件的內(nèi)核緩沖區(qū),然后每隔一秒將緩沖區(qū)里的內(nèi)容寫回到硬盤;

No,意味著不由 Redis 控制寫回硬盤的時(shí)機(jī),轉(zhuǎn)交給操作系統(tǒng)控制寫回的時(shí)機(jī),也就是每次寫操作命令執(zhí)行完后,先將命令寫入到 AOF 文件的內(nèi)核緩沖區(qū),再由操作系統(tǒng)決定何時(shí)將緩沖區(qū)內(nèi)容寫回硬盤。

我也把這 3 個(gè)寫回策略的優(yōu)缺點(diǎn)總結(jié)成了一張表格:

AOF 日志過大,會(huì)觸發(fā)什么機(jī)制?

AOF 日志是一個(gè)文件,隨著執(zhí)行的寫操作命令越來越多,文件的大小會(huì)越來越大。
如果當(dāng) AOF 日志文件過大就會(huì)帶來性能問題,比如重啟 Redis 后,需要讀 AOF 文件的內(nèi)容以恢復(fù)數(shù)據(jù),如果文件過大,整個(gè)恢復(fù)的過程就會(huì)很慢。

所以,Redis 為了避免 AOF 文件越寫越大,提供了 AOF 重寫機(jī)制,當(dāng) AOF 文件的大小超過所設(shè)定的閾值后,Redis 就會(huì)啟用 AOF 重寫機(jī)制,來壓縮 AOF 文件。

AOF 重寫機(jī)制是在重寫時(shí),讀取當(dāng)前數(shù)據(jù)庫(kù)中的所有鍵值對(duì),然后將每一個(gè)鍵值對(duì)用一條命令記錄到「新的 AOF 文件」,等到全部記錄完后,就將新的 AOF 文件替換掉現(xiàn)有的 AOF 文件。

舉個(gè)例子,在沒有使用重寫機(jī)制前,假設(shè)前后執(zhí)行了「_set name xiaolin_」和「_set name xiaolincoding_」這兩個(gè)命令的話,就會(huì)將這兩個(gè)命令記錄到 AOF 文件。

但是在使用重寫機(jī)制后,就會(huì)讀取 name 最新的 value(鍵值對(duì)) ,然后用一條 「set name xiaolincoding」命令記錄到新的 AOF 文件,之前的第一個(gè)命令就沒有必要記錄了,因?yàn)樗鼘儆凇笟v史」命令,沒有作用了。這樣一來,一個(gè)鍵值對(duì)在重寫日志中只用一條命令就行了。

重寫工作完成后,就會(huì)將新的 AOF 文件覆蓋現(xiàn)有的 AOF 文件,這就相當(dāng)于壓縮了 AOF 文件,使得 AOF 文件體積變小了。

重寫 AOF 日志的過程是怎樣的?

Redis 的重寫 AOF 過程是由后臺(tái)子進(jìn)程 bgrewriteaof 來完成的,這么做可以達(dá)到兩個(gè)好處:

    子進(jìn)程進(jìn)行 AOF 重寫期間,主進(jìn)程可以繼續(xù)處理命令請(qǐng)求,從而避免阻塞主進(jìn)程;子進(jìn)程帶有主進(jìn)程的數(shù)據(jù)副本,這里使用子進(jìn)程而不是線程,因?yàn)槿绻鞘褂镁€程,多線程之間會(huì)共享內(nèi)存,那么在修改共享內(nèi)存數(shù)據(jù)的時(shí)候,需要通過加鎖來保證數(shù)據(jù)的安全,而這樣就會(huì)降低性能。而使用子進(jìn)程,創(chuàng)建子進(jìn)程時(shí),父子進(jìn)程是共享內(nèi)存數(shù)據(jù)的,不過這個(gè)共享的內(nèi)存只能以只讀的方式,而當(dāng)父子進(jìn)程任意一方修改了該共享內(nèi)存,就會(huì)發(fā)生「寫時(shí)復(fù)制」,于是父子進(jìn)程就有了獨(dú)立的數(shù)據(jù)副本,就不用加鎖來保證數(shù)據(jù)安全

觸發(fā)重寫機(jī)制后,主進(jìn)程就會(huì)創(chuàng)建重寫 AOF 的子進(jìn)程,此時(shí)父子進(jìn)程共享物理內(nèi)存,重寫子進(jìn)程只會(huì)對(duì)這個(gè)內(nèi)存進(jìn)行只讀,重寫 AOF 子進(jìn)程會(huì)讀取數(shù)據(jù)庫(kù)里的所有數(shù)據(jù),并逐一把內(nèi)存數(shù)據(jù)的鍵值對(duì)轉(zhuǎn)換成一條命令,再將命令記錄到重寫日志(新的 AOF 文件)。

但是重寫過程中,主進(jìn)程依然可以正常處理命令,那問題來了,重寫 AOF 日志過程中,如果主進(jìn)程修改了已經(jīng)存在 key-value,那么會(huì)發(fā)生寫時(shí)復(fù)制,此時(shí)這個(gè) key-value 數(shù)據(jù)在子進(jìn)程的內(nèi)存數(shù)據(jù)就跟主進(jìn)程的內(nèi)存數(shù)據(jù)不一致了,這時(shí)要怎么辦呢?

為了解決這種數(shù)據(jù)不一致問題,Redis 設(shè)置了一個(gè) AOF 重寫緩沖區(qū),這個(gè)緩沖區(qū)在創(chuàng)建 bgrewriteaof 子進(jìn)程之后開始使用。

在重寫 AOF 期間,當(dāng) Redis 執(zhí)行完一個(gè)寫命令之后,它會(huì)同時(shí)將這個(gè)寫命令寫入到 「AOF 緩沖區(qū)」和 「AOF 重寫緩沖區(qū)」

也就是說,在 bgrewriteaof 子進(jìn)程執(zhí)行 AOF 重寫期間,主進(jìn)程需要執(zhí)行以下三個(gè)工作:

    執(zhí)行客戶端發(fā)來的命令;將執(zhí)行后的寫命令追加到 「AOF 緩沖區(qū)」;將執(zhí)行后的寫命令追加到 「AOF 重寫緩沖區(qū)」;

當(dāng)子進(jìn)程完成 AOF 重寫工作(_掃描數(shù)據(jù)庫(kù)中所有數(shù)據(jù),逐一把內(nèi)存數(shù)據(jù)的鍵值對(duì)轉(zhuǎn)換成一條命令,再將命令記錄到重寫日志_)后,會(huì)向主進(jìn)程發(fā)送一條信號(hào),信號(hào)是進(jìn)程間通訊的一種方式,且是異步的。

主進(jìn)程收到該信號(hào)后,會(huì)調(diào)用一個(gè)信號(hào)處理函數(shù),該函數(shù)主要做以下工作:

    將 AOF 重寫緩沖區(qū)中的所有內(nèi)容追加到新的 AOF 的文件中,使得新舊兩個(gè) AOF 文件所保存的數(shù)據(jù)庫(kù)狀態(tài)一致;新的 AOF 的文件進(jìn)行改名,覆蓋現(xiàn)有的 AOF 文件。

信號(hào)函數(shù)執(zhí)行完后,主進(jìn)程就可以繼續(xù)像往常一樣處理命令了。

::: tip

AOF 日志的內(nèi)容就暫時(shí)提這些,想更詳細(xì)了解 AOF 日志的工作原理,可以詳細(xì)看這篇:AOF 持久化是怎么實(shí)現(xiàn)的

RDB 快照是如何實(shí)現(xiàn)的呢?

因?yàn)?AOF 日志記錄的是操作命令,不是實(shí)際的數(shù)據(jù),所以用 AOF 方法做故障恢復(fù)時(shí),需要全量把日志都執(zhí)行一遍,一旦 AOF 日志非常多,勢(shì)必會(huì)造成 Redis 的恢復(fù)操作緩慢。

為了解決這個(gè)問題,Redis 增加了 RDB 快照。所謂的快照,就是記錄某一個(gè)瞬間東西,比如當(dāng)我們給風(fēng)景拍照時(shí),那一個(gè)瞬間的畫面和信息就記錄到了一張照片。

所以,RDB 快照就是記錄某一個(gè)瞬間的內(nèi)存數(shù)據(jù),記錄的是實(shí)際數(shù)據(jù),而 AOF 文件記錄的是命令操作的日志,而不是實(shí)際的數(shù)據(jù)。

因此在 Redis 恢復(fù)數(shù)據(jù)時(shí), RDB 恢復(fù)數(shù)據(jù)的效率會(huì)比 AOF 高些,因?yàn)橹苯訉?RDB 文件讀入內(nèi)存就可以,不需要像 AOF 那樣還需要額外執(zhí)行操作命令的步驟才能恢復(fù)數(shù)據(jù)。

RDB 做快照時(shí)會(huì)阻塞線程嗎?

Redis 提供了兩個(gè)命令來生成 RDB 文件,分別是 save 和 bgsave,他們的區(qū)別就在于是否在「主線程」里執(zhí)行:

    • 執(zhí)行了 save 命令,就會(huì)在主線程生成 RDB 文件,由于和執(zhí)行操作命令在同一個(gè)線程,所以如果寫入 RDB 文件的時(shí)間太長(zhǎng),

會(huì)阻塞主線程;執(zhí)行了 bgsave 命令,會(huì)創(chuàng)建一個(gè)子進(jìn)程來生成 RDB 文件,這樣可以

避免主線程的阻塞;

Redis 還可以通過配置文件的選項(xiàng)來實(shí)現(xiàn)每隔一段時(shí)間自動(dòng)執(zhí)行一次 bgsave 命令,默認(rèn)會(huì)提供以下配置:

save?900?1
save?300?10
save?60?10000

別看選項(xiàng)名叫 save,實(shí)際上執(zhí)行的是 bgsave 命令,也就是會(huì)創(chuàng)建子進(jìn)程來生成 RDB 快照文件。
只要滿足上面條件的任意一個(gè),就會(huì)執(zhí)行 bgsave,它們的意思分別是:

    900 秒之內(nèi),對(duì)數(shù)據(jù)庫(kù)進(jìn)行了至少 1 次修改;300 秒之內(nèi),對(duì)數(shù)據(jù)庫(kù)進(jìn)行了至少 10 次修改;60 秒之內(nèi),對(duì)數(shù)據(jù)庫(kù)進(jìn)行了至少 10000 次修改。

這里提一點(diǎn),Redis 的快照是全量快照,也就是說每次執(zhí)行快照,都是把內(nèi)存中的「所有數(shù)據(jù)」都記錄到磁盤中。所以執(zhí)行快照是一個(gè)比較重的操作,如果頻率太頻繁,可能會(huì)對(duì) Redis 性能產(chǎn)生影響。如果頻率太低,服務(wù)器故障時(shí),丟失的數(shù)據(jù)會(huì)更多。

RDB 在執(zhí)行快照的時(shí)候,數(shù)據(jù)能修改嗎?

可以的,執(zhí)行 bgsave 過程中,Redis 依然可以繼續(xù)處理操作命令的,也就是數(shù)據(jù)是能被修改的,關(guān)鍵的技術(shù)就在于寫時(shí)復(fù)制技術(shù)(Copy-On-Write, COW)。

執(zhí)行 bgsave 命令的時(shí)候,會(huì)通過 fork() 創(chuàng)建子進(jìn)程,此時(shí)子進(jìn)程和父進(jìn)程是共享同一片內(nèi)存數(shù)據(jù)的,因?yàn)閯?chuàng)建子進(jìn)程的時(shí)候,會(huì)復(fù)制父進(jìn)程的頁(yè)表,但是頁(yè)表指向的物理內(nèi)存還是一個(gè),此時(shí)如果主線程執(zhí)行讀操作,則主線程和 bgsave 子進(jìn)程互相不影響。

如果主線程執(zhí)行寫操作,則被修改的數(shù)據(jù)會(huì)復(fù)制一份副本,然后 bgsave 子進(jìn)程會(huì)把該副本數(shù)據(jù)寫入 RDB 文件,在這個(gè)過程中,主線程仍然可以直接修改原來的數(shù)據(jù)。

::: tip

RDB 快照的內(nèi)容就暫時(shí)提這些,想更詳細(xì)了解 RDB 快照的工作原理,可以詳細(xì)看這篇:RDB 快照是怎么實(shí)現(xiàn)的?

為什么會(huì)有混合持久化?

RDB 優(yōu)點(diǎn)是數(shù)據(jù)恢復(fù)速度快,但是快照的頻率不好把握。頻率太低,丟失的數(shù)據(jù)就會(huì)比較多,頻率太高,就會(huì)影響性能。

AOF 優(yōu)點(diǎn)是丟失數(shù)據(jù)少,但是數(shù)據(jù)恢復(fù)不快。

為了集成了兩者的優(yōu)點(diǎn), Redis 4.0 提出了混合使用 AOF 日志和內(nèi)存快照,也叫混合持久化,既保證了 Redis 重啟速度,又降低數(shù)據(jù)丟失風(fēng)險(xiǎn)。

混合持久化工作在 AOF 日志重寫過程,當(dāng)開啟了混合持久化時(shí),在 AOF 重寫日志時(shí),fork 出來的重寫子進(jìn)程會(huì)先將與主線程共享的內(nèi)存數(shù)據(jù)以 RDB 方式寫入到 AOF 文件,然后主線程處理的操作命令會(huì)被記錄在重寫緩沖區(qū)里,重寫緩沖區(qū)里的增量命令會(huì)以 AOF 方式寫入到 AOF 文件,寫入完成后通知主進(jìn)程將新的含有 RDB 格式和 AOF 格式的 AOF 文件替換舊的的 AOF 文件。

也就是說,使用了混合持久化,AOF 文件的前半部分是 RDB 格式的全量數(shù)據(jù),后半部分是 AOF 格式的增量數(shù)據(jù)。

這樣的好處在于,重啟 Redis 加載數(shù)據(jù)的時(shí)候,由于前半部分是 RDB 內(nèi)容,這樣加載的時(shí)候速度會(huì)很快

加載完 RDB 的內(nèi)容后,才會(huì)加載后半部分的 AOF 內(nèi)容,這里的內(nèi)容是 Redis 后臺(tái)子進(jìn)程重寫 AOF 期間,主線程處理的操作命令,可以使得數(shù)據(jù)更少的丟失。

混合持久化優(yōu)點(diǎn):混合持久化結(jié)合了 RDB 和 AOF 持久化的優(yōu)點(diǎn),開頭為 RDB 的格式,使得 Redis 可以更快的啟動(dòng),同時(shí)結(jié)合 AOF 的優(yōu)點(diǎn),有減低了大量數(shù)據(jù)丟失的風(fēng)險(xiǎn)。

混合持久化缺點(diǎn):AOF 文件中添加了 RDB 格式的內(nèi)容,使得 AOF 文件的可讀性變得很差;兼容性差,如果開啟混合持久化,那么此混合持久化 AOF 文件,就不能用在 Redis 4.0 之前版本了。

Redis 集群

Redis 如何實(shí)現(xiàn)服務(wù)高可用?

要想設(shè)計(jì)一個(gè)高可用的 Redis 服務(wù),一定要從 Redis 的多服務(wù)節(jié)點(diǎn)來考慮,比如 Redis 的主從復(fù)制、哨兵模式、切片集群。

主從復(fù)制

主從復(fù)制是 Redis 高可用服務(wù)的最基礎(chǔ)的保證,實(shí)現(xiàn)方案就是將從前的一臺(tái) Redis 服務(wù)器,同步數(shù)據(jù)到多臺(tái)從 Redis 服務(wù)器上,即一主多從的模式,且主從服務(wù)器之間采用的是「讀寫分離」的方式。

主服務(wù)器可以進(jìn)行讀寫操作,當(dāng)發(fā)生寫操作時(shí)自動(dòng)將寫操作同步給從服務(wù)器,而從服務(wù)器一般是只讀,并接受主服務(wù)器同步過來寫操作命令,然后執(zhí)行這條命令。

也就是說,所有的數(shù)據(jù)修改只在主服務(wù)器上進(jìn)行,然后將最新的數(shù)據(jù)同步給從服務(wù)器,這樣就使得主從服務(wù)器的數(shù)據(jù)是一致的。

注意,主從服務(wù)器之間的命令復(fù)制是異步進(jìn)行的。

具體來說,在主從服務(wù)器命令傳播階段,主服務(wù)器收到新的寫命令后,會(huì)發(fā)送給從服務(wù)器。但是,主服務(wù)器并不會(huì)等到從服務(wù)器實(shí)際執(zhí)行完命令后,再把結(jié)果返回給客戶端,而是主服務(wù)器自己在本地執(zhí)行完命令后,就會(huì)向客戶端返回結(jié)果了。如果從服務(wù)器還沒有執(zhí)行主服務(wù)器同步過來的命令,主從服務(wù)器間的數(shù)據(jù)就不一致了。

所以,無法實(shí)現(xiàn)強(qiáng)一致性保證(主從數(shù)據(jù)時(shí)時(shí)刻刻保持一致),數(shù)據(jù)不一致是難以避免的。

::: tip

想更詳細(xì)了解 ?Redis 主從復(fù)制的工作原理,可以詳細(xì)看這篇:主從復(fù)制是怎么實(shí)現(xiàn)的?

哨兵模式

在使用 Redis 主從服務(wù)的時(shí)候,會(huì)有一個(gè)問題,就是當(dāng) Redis 的主從服務(wù)器出現(xiàn)故障宕機(jī)時(shí),需要手動(dòng)進(jìn)行恢復(fù)。

為了解決這個(gè)問題,Redis 增加了哨兵模式(Redis Sentinel),因?yàn)樯诒J阶龅搅丝梢员O(jiān)控主從服務(wù)器,并且提供主從節(jié)點(diǎn)故障轉(zhuǎn)移的功能。

切片集群模式

當(dāng) Redis 緩存數(shù)據(jù)量大到一臺(tái)服務(wù)器無法緩存時(shí),就需要使用 Redis 切片集群(Redis Cluster )方案,它將數(shù)據(jù)分布在不同的服務(wù)器上,以此來降低系統(tǒng)對(duì)單主節(jié)點(diǎn)的依賴,從而提高 Redis 服務(wù)的讀寫性能。

Redis Cluster 方案采用哈希槽(Hash Slot),來處理數(shù)據(jù)和節(jié)點(diǎn)之間的映射關(guān)系。在 Redis Cluster 方案中,一個(gè)切片集群共有 16384 個(gè)哈希槽,這些哈希槽類似于數(shù)據(jù)分區(qū),每個(gè)鍵值對(duì)都會(huì)根據(jù)它的 key,被映射到一個(gè)哈希槽中,具體執(zhí)行過程分為兩大步:

    根據(jù)鍵值對(duì)的 key,按照 CRC16 算法計(jì)算一個(gè) 16 bit 的值。再用 16bit 值對(duì) 16384 取模,得到 0~16383 范圍內(nèi)的模數(shù),每個(gè)模數(shù)代表一個(gè)相應(yīng)編號(hào)的哈希槽。

接下來的問題就是,這些哈希槽怎么被映射到具體的 Redis 節(jié)點(diǎn)上的呢?有兩種方案:

平均分配:

    • 在使用 cluster create 命令創(chuàng)建 Redis 集群時(shí),Redis 會(huì)自動(dòng)把所有哈希槽平均分布到集群節(jié)點(diǎn)上。比如集群中有 9 個(gè)節(jié)點(diǎn),則每個(gè)節(jié)點(diǎn)上槽的個(gè)數(shù)為 16384/9 個(gè)。

手動(dòng)分配:

    可以使用 cluster meet 命令手動(dòng)建立節(jié)點(diǎn)間的連接,組成集群,再使用 cluster addslots 命令,指定每個(gè)節(jié)點(diǎn)上的哈希槽個(gè)數(shù)。

為了方便你的理解,我通過一張圖來解釋數(shù)據(jù)、哈希槽,以及節(jié)點(diǎn)三者的映射分布關(guān)系。

上圖中的切片集群一共有 2 個(gè)節(jié)點(diǎn),假設(shè)有 4 個(gè)哈希槽(Slot 0~Slot 3)時(shí),我們就可以通過命令手動(dòng)分配哈希槽,比如節(jié)點(diǎn) 1 保存哈希槽 0 和 1,節(jié)點(diǎn) 2 保存哈希槽 2 和 3。

redis-cli?-h?192.168.1.10?–p?6379?cluster?addslots?0,1
redis-cli?-h?192.168.1.11?–p?6379?cluster?addslots?2,3

然后在集群運(yùn)行的過程中,key1 和 key2 計(jì)算完 CRC16 值后,對(duì)哈希槽總個(gè)數(shù) 4 進(jìn)行取模,再根據(jù)各自的模數(shù)結(jié)果,就可以被映射到哈希槽 1(對(duì)應(yīng)節(jié)點(diǎn)1) 和 哈希槽 2(對(duì)應(yīng)節(jié)點(diǎn)2)。

需要注意的是,在手動(dòng)分配哈希槽時(shí),需要把 16384 個(gè)槽都分配完,否則 Redis 集群無法正常工作。

集群腦裂導(dǎo)致數(shù)據(jù)丟失怎么辦?

什么是腦裂?

先來理解集群的腦裂現(xiàn)象,這就好比一個(gè)人有兩個(gè)大腦,那么到底受誰控制呢?

那么在 Redis 中,集群腦裂產(chǎn)生數(shù)據(jù)丟失的現(xiàn)象是怎樣的呢?

在 Redis 主從架構(gòu)中,部署方式一般是「一主多從」,主節(jié)點(diǎn)提供寫操作,從節(jié)點(diǎn)提供讀操作。
如果主節(jié)點(diǎn)的網(wǎng)絡(luò)突然發(fā)生了問題,它與所有的從節(jié)點(diǎn)都失聯(lián)了,但是此時(shí)的主節(jié)點(diǎn)和客戶端的網(wǎng)絡(luò)是正常的,這個(gè)客戶端并不知道 Redis 內(nèi)部已經(jīng)出現(xiàn)了問題,還在照樣的向這個(gè)失聯(lián)的主節(jié)點(diǎn)寫數(shù)據(jù)(過程A),此時(shí)這些數(shù)據(jù)被舊主節(jié)點(diǎn)緩存到了緩沖區(qū)里,因?yàn)橹鲝墓?jié)點(diǎn)之間的網(wǎng)絡(luò)問題,這些數(shù)據(jù)都是無法同步給從節(jié)點(diǎn)的。

這時(shí),哨兵也發(fā)現(xiàn)主節(jié)點(diǎn)失聯(lián)了,它就認(rèn)為主節(jié)點(diǎn)掛了(但實(shí)際上主節(jié)點(diǎn)正常運(yùn)行,只是網(wǎng)絡(luò)出問題了),于是哨兵就會(huì)在「從節(jié)點(diǎn)」中選舉出一個(gè) leader 作為主節(jié)點(diǎn),這時(shí)集群就有兩個(gè)主節(jié)點(diǎn)了 —— 腦裂出現(xiàn)了。

然后,網(wǎng)絡(luò)突然好了,哨兵因?yàn)橹耙呀?jīng)選舉出一個(gè)新主節(jié)點(diǎn)了,它就會(huì)把舊主節(jié)點(diǎn)降級(jí)為從節(jié)點(diǎn)(A),然后從節(jié)點(diǎn)(A)會(huì)向新主節(jié)點(diǎn)請(qǐng)求數(shù)據(jù)同步,因?yàn)榈谝淮瓮绞侨客降姆绞剑藭r(shí)的從節(jié)點(diǎn)(A)會(huì)清空掉自己本地的數(shù)據(jù),然后再做全量同步。所以,之前客戶端在過程 A 寫入的數(shù)據(jù)就會(huì)丟失了,也就是集群產(chǎn)生腦裂數(shù)據(jù)丟失的問題

總結(jié)一句話就是:由于網(wǎng)絡(luò)問題,集群節(jié)點(diǎn)之間失去聯(lián)系。主從數(shù)據(jù)不同步;重新平衡選舉,產(chǎn)生兩個(gè)主服務(wù)。等網(wǎng)絡(luò)恢復(fù),舊主節(jié)點(diǎn)會(huì)降級(jí)為從節(jié)點(diǎn),再與新主節(jié)點(diǎn)進(jìn)行同步復(fù)制的時(shí)候,由于會(huì)從節(jié)點(diǎn)會(huì)清空自己的緩沖區(qū),所以導(dǎo)致之前客戶端寫入的數(shù)據(jù)丟失了。

解決方案

當(dāng)主節(jié)點(diǎn)發(fā)現(xiàn)從節(jié)點(diǎn)下線或者通信超時(shí)的總數(shù)量小于閾值時(shí),那么禁止主節(jié)點(diǎn)進(jìn)行寫數(shù)據(jù),直接把錯(cuò)誤返回給客戶端。

在 Redis 的配置文件中有兩個(gè)參數(shù)我們可以設(shè)置:

    min-slaves-to-write x,主節(jié)點(diǎn)必須要有至少 x 個(gè)從節(jié)點(diǎn)連接,如果小于這個(gè)數(shù),主節(jié)點(diǎn)會(huì)禁止寫數(shù)據(jù)。min-slaves-max-lag x,主從數(shù)據(jù)復(fù)制和同步的延遲不能超過 x 秒,如果超過,主節(jié)點(diǎn)會(huì)禁止寫數(shù)據(jù)。

我們可以把 min-slaves-to-write 和 min-slaves-max-lag 這兩個(gè)配置項(xiàng)搭配起來使用,分別給它們?cè)O(shè)置一定的閾值,假設(shè)為 N 和 T。

這兩個(gè)配置項(xiàng)組合后的要求是,主庫(kù)連接的從庫(kù)中至少有 N 個(gè)從庫(kù),和主庫(kù)進(jìn)行數(shù)據(jù)復(fù)制時(shí)的 ACK 消息延遲不能超過 T 秒,否則,主庫(kù)就不會(huì)再接收客戶端的寫請(qǐng)求了。

即使原主庫(kù)是假故障,它在假故障期間也無法響應(yīng)哨兵心跳,也不能和從庫(kù)進(jìn)行同步,自然也就無法和從庫(kù)進(jìn)行 ACK 確認(rèn)了。這樣一來,min-slaves-to-write 和 min-slaves-max-lag 的組合要求就無法得到滿足,原主庫(kù)就會(huì)被限制接收客戶端寫請(qǐng)求,客戶端也就不能在原主庫(kù)中寫入新數(shù)據(jù)了。

等到新主庫(kù)上線時(shí),就只有新主庫(kù)能接收和處理客戶端請(qǐng)求,此時(shí),新寫的數(shù)據(jù)會(huì)被直接寫到新主庫(kù)中。而原主庫(kù)會(huì)被哨兵降為從庫(kù),即使它的數(shù)據(jù)被清空了,也不會(huì)有新數(shù)據(jù)丟失。

再來舉個(gè)例子。

假設(shè)我們將 min-slaves-to-write 設(shè)置為 1,把 min-slaves-max-lag 設(shè)置為 12s,把哨兵的 down-after-milliseconds 設(shè)置為 10s,主庫(kù)因?yàn)槟承┰蚩ㄗ×?15s,導(dǎo)致哨兵判斷主庫(kù)客觀下線,開始進(jìn)行主從切換。

同時(shí),因?yàn)樵鲙?kù)卡住了 15s,沒有一個(gè)從庫(kù)能和原主庫(kù)在 12s 內(nèi)進(jìn)行數(shù)據(jù)復(fù)制,原主庫(kù)也無法接收客戶端請(qǐng)求了。

這樣一來,主從切換完成后,也只有新主庫(kù)能接收請(qǐng)求,不會(huì)發(fā)生腦裂,也就不會(huì)發(fā)生數(shù)據(jù)丟失的問題了。

Redis 過期刪除與內(nèi)存淘汰

Redis 使用的過期刪除策略是什么?

Redis 是可以對(duì) key 設(shè)置過期時(shí)間的,因此需要有相應(yīng)的機(jī)制將已過期的鍵值對(duì)刪除,而做這個(gè)工作的就是過期鍵值刪除策略。

每當(dāng)我們對(duì)一個(gè) key 設(shè)置了過期時(shí)間時(shí),Redis 會(huì)把該 key 帶上過期時(shí)間存儲(chǔ)到一個(gè)過期字典(expires dict)中,也就是說「過期字典」保存了數(shù)據(jù)庫(kù)中所有 key 的過期時(shí)間。

當(dāng)我們查詢一個(gè) key 時(shí),Redis 首先檢查該 key 是否存在于過期字典中:

    如果不在,則正常讀取鍵值;如果存在,則會(huì)獲取該 key 的過期時(shí)間,然后與當(dāng)前系統(tǒng)時(shí)間進(jìn)行比對(duì),如果比系統(tǒng)時(shí)間大,那就沒有過期,否則判定該 key 已過期。

Redis 使用的過期刪除策略是「惰性刪除+定期刪除」這兩種策略配和使用。

什么是惰性刪除策略?

惰性刪除策略的做法是,不主動(dòng)刪除過期鍵,每次從數(shù)據(jù)庫(kù)訪問 key 時(shí),都檢測(cè) key 是否過期,如果過期則刪除該 key。

惰性刪除的流程圖如下:

惰性刪除策略的優(yōu)點(diǎn):因?yàn)槊看卧L問時(shí),才會(huì)檢查 key 是否過期,所以此策略只會(huì)使用很少的系統(tǒng)資源,因此,惰性刪除策略對(duì) CPU 時(shí)間最友好。

惰性刪除策略的缺點(diǎn):如果一個(gè) key 已經(jīng)過期,而這個(gè) key 又仍然保留在數(shù)據(jù)庫(kù)中,那么只要這個(gè)過期 key 一直沒有被訪問,它所占用的內(nèi)存就不會(huì)釋放,造成了一定的內(nèi)存空間浪費(fèi)。所以,惰性刪除策略對(duì)內(nèi)存不友好。

什么是定期刪除策略?

定期刪除策略的做法是,每隔一段時(shí)間「隨機(jī)」從數(shù)據(jù)庫(kù)中取出一定數(shù)量的 key 進(jìn)行檢查,并刪除其中的過期key。

Redis 的定期刪除的流程:

    從過期字典中隨機(jī)抽取 20 個(gè) key;檢查這 20 個(gè) key 是否過期,并刪除已過期的 key;如果本輪檢查的已過期 key 的數(shù)量,超過 5 個(gè)(20/4),也就是「已過期 key 的數(shù)量」占比「隨機(jī)抽取 key 的數(shù)量」大于 25%,則繼續(xù)重復(fù)步驟 1;如果已過期的 key 比例小于 25%,則停止繼續(xù)刪除過期 key,然后等待下一輪再檢查。

可以看到,定期刪除是一個(gè)循環(huán)的流程。那 Redis 為了保證定期刪除不會(huì)出現(xiàn)循環(huán)過度,導(dǎo)致線程卡死現(xiàn)象,為此增加了定期刪除循環(huán)流程的時(shí)間上限,默認(rèn)不會(huì)超過 25ms。

定期刪除的流程如下:

定期刪除策略的優(yōu)點(diǎn):通過限制刪除操作執(zhí)行的時(shí)長(zhǎng)和頻率,來減少刪除操作對(duì) CPU 的影響,同時(shí)也能刪除一部分過期的數(shù)據(jù)減少了過期鍵對(duì)空間的無效占用。

定期刪除策略的缺點(diǎn):難以確定刪除操作執(zhí)行的時(shí)長(zhǎng)和頻率。如果執(zhí)行的太頻繁,就會(huì)對(duì) CPU 不友好;如果執(zhí)行的太少,那又和惰性刪除一樣了,過期 key 占用的內(nèi)存不會(huì)及時(shí)得到釋放。

可以看到,惰性刪除策略和定期刪除策略都有各自的優(yōu)點(diǎn),所以 Redis 選擇「惰性刪除+定期刪除」這兩種策略配和使用,以求在合理使用 CPU 時(shí)間和避免內(nèi)存浪費(fèi)之間取得平衡。

Redis 持久化時(shí),對(duì)過期鍵會(huì)如何處理的?

Redis 持久化文件有兩種格式:RDB(Redis Database)和 AOF(Append Only File),下面我們分別來看過期鍵在這兩種格式中的呈現(xiàn)狀態(tài)。

RDB 文件分為兩個(gè)階段,RDB 文件生成階段和加載階段。

RDB 文件生成階段:從內(nèi)存狀態(tài)持久化成 RDB(文件)的時(shí)候,會(huì)對(duì) key 進(jìn)行過期檢查,

過期的鍵「不會(huì)」被保存到新的 RDB 文件中,因此 Redis 中的過期鍵不會(huì)對(duì)生成新 RDB 文件產(chǎn)生任何影響。

RDB 加載階段:RDB 加載階段時(shí),要看服務(wù)器是主服務(wù)器還是從服務(wù)器,分別對(duì)應(yīng)以下兩種情況:

如果 Redis 是「主服務(wù)器」運(yùn)行模式的話,在載入 RDB 文件時(shí),程序會(huì)對(duì)文件中保存的鍵進(jìn)行檢查,過期鍵「不會(huì)」被載入到數(shù)據(jù)庫(kù)中。所以過期鍵不會(huì)對(duì)載入 RDB 文件的主服務(wù)器造成影響;

如果 Redis 是「從服務(wù)器」運(yùn)行模式的話,在載入 RDB 文件時(shí),不論鍵是否過期都會(huì)被載入到數(shù)據(jù)庫(kù)中。但由于主從服務(wù)器在進(jìn)行數(shù)據(jù)同步時(shí),從服務(wù)器的數(shù)據(jù)會(huì)被清空。所以一般來說,過期鍵對(duì)載入 RDB 文件的從服務(wù)器也不會(huì)造成影響。

AOF 文件分為兩個(gè)階段,AOF 文件寫入階段和 AOF 重寫階段。

AOF 文件寫入階段:當(dāng) Redis 以 AOF 模式持久化時(shí),如果數(shù)據(jù)庫(kù)某個(gè)過期鍵還沒被刪除,那么 AOF 文件會(huì)保留此過期鍵,當(dāng)此過期鍵被刪除后,Redis 會(huì)向 AOF 文件追加一條 DEL 命令來顯式地刪除該鍵值。

AOF 重寫階段:執(zhí)行 AOF 重寫時(shí),會(huì)對(duì) Redis 中的鍵值對(duì)進(jìn)行檢查,已過期的鍵不會(huì)被保存到重寫后的 AOF 文件中,因此不會(huì)對(duì) AOF 重寫造成任何影響。

Redis 主從模式中,對(duì)過期鍵會(huì)如何處理?

當(dāng) Redis 運(yùn)行在主從模式下時(shí),從庫(kù)不會(huì)進(jìn)行過期掃描,從庫(kù)對(duì)過期的處理是被動(dòng)的。也就是即使從庫(kù)中的 key 過期了,如果有客戶端訪問從庫(kù)時(shí),依然可以得到 key 對(duì)應(yīng)的值,像未過期的鍵值對(duì)一樣返回。

從庫(kù)的過期鍵處理依靠主服務(wù)器控制,主庫(kù)在 key 到期時(shí),會(huì)在 AOF 文件里增加一條 del 指令,同步到所有的從庫(kù),從庫(kù)通過執(zhí)行這條 del 指令來刪除過期的 key。

Redis 內(nèi)存滿了,會(huì)發(fā)生什么?

在 Redis 的運(yùn)行內(nèi)存達(dá)到了某個(gè)閥值,就會(huì)觸發(fā)內(nèi)存淘汰機(jī)制,這個(gè)閥值就是我們?cè)O(shè)置的最大運(yùn)行內(nèi)存,此值在 Redis 的配置文件中可以找到,配置項(xiàng)為 maxmemory。

Redis 內(nèi)存淘汰策略有哪些?

Redis 內(nèi)存淘汰策略共有八種,這八種策略大體分為「不進(jìn)行數(shù)據(jù)淘汰」和「進(jìn)行數(shù)據(jù)淘汰」兩類策略。

1、不進(jìn)行數(shù)據(jù)淘汰的策略

noeviction(Redis3.0之后,默認(rèn)的內(nèi)存淘汰策略) :它表示當(dāng)運(yùn)行內(nèi)存超過最大設(shè)置內(nèi)存時(shí),不淘汰任何數(shù)據(jù),而是不再提供服務(wù),直接返回錯(cuò)誤。

2、進(jìn)行數(shù)據(jù)淘汰的策略

針對(duì)「進(jìn)行數(shù)據(jù)淘汰」這一類策略,又可以細(xì)分為「在設(shè)置了過期時(shí)間的數(shù)據(jù)中進(jìn)行淘汰」和「在所有數(shù)據(jù)范圍內(nèi)進(jìn)行淘汰」這兩類策略。
在設(shè)置了過期時(shí)間的數(shù)據(jù)中進(jìn)行淘汰:

volatile-random:隨機(jī)淘汰設(shè)置了過期時(shí)間的任意鍵值;

volatile-ttl:優(yōu)先淘汰更早過期的鍵值。

volatile-lru(Redis3.0 之前,默認(rèn)的內(nèi)存淘汰策略):淘汰所有設(shè)置了過期時(shí)間的鍵值中,最久未使用的鍵值;

volatile-lfu(Redis 4.0 后新增的內(nèi)存淘汰策略):淘汰所有設(shè)置了過期時(shí)間的鍵值中,最少使用的鍵值;

在所有數(shù)據(jù)范圍內(nèi)進(jìn)行淘汰:

allkeys-random:隨機(jī)淘汰任意鍵值;

allkeys-lru:淘汰整個(gè)鍵值中最久未使用的鍵值;

allkeys-lfu(Redis 4.0 后新增的內(nèi)存淘汰策略):淘汰整個(gè)鍵值中最少使用的鍵值。

LRU 算法和 LFU 算法有什么區(qū)別?

什么是 LRU 算法?

LRU 全稱是 Least Recently Used 翻譯為最近最少使用,會(huì)選擇淘汰最近最少使用的數(shù)據(jù)。

傳統(tǒng) LRU 算法的實(shí)現(xiàn)是基于「鏈表」結(jié)構(gòu),鏈表中的元素按照操作順序從前往后排列,最新操作的鍵會(huì)被移動(dòng)到表頭,當(dāng)需要內(nèi)存淘汰時(shí),只需要?jiǎng)h除鏈表尾部的元素即可,因?yàn)殒湵砦膊康脑鼐痛碜罹梦幢皇褂玫脑亍?/p>

Redis 并沒有使用這樣的方式實(shí)現(xiàn) LRU 算法,因?yàn)閭鹘y(tǒng)的 LRU 算法存在兩個(gè)問題:

    需要用鏈表管理所有的緩存數(shù)據(jù),這會(huì)帶來額外的空間開銷;當(dāng)有數(shù)據(jù)被訪問時(shí),需要在鏈表上把該數(shù)據(jù)移動(dòng)到頭端,如果有大量數(shù)據(jù)被訪問,就會(huì)帶來很多鏈表移動(dòng)操作,會(huì)很耗時(shí),進(jìn)而會(huì)降低 Redis 緩存性能。

Redis 是如何實(shí)現(xiàn) LRU 算法的?

Redis 實(shí)現(xiàn)的是一種近似 LRU 算法,目的是為了更好的節(jié)約內(nèi)存,它的實(shí)現(xiàn)方式是在 Redis 的對(duì)象結(jié)構(gòu)體中添加一個(gè)額外的字段,用于記錄此數(shù)據(jù)的最后一次訪問時(shí)間。

當(dāng) Redis 進(jìn)行內(nèi)存淘汰時(shí),會(huì)使用隨機(jī)采樣的方式來淘汰數(shù)據(jù),它是隨機(jī)取 5 個(gè)值(此值可配置),然后淘汰最久沒有使用的那個(gè)

Redis 實(shí)現(xiàn)的 LRU 算法的優(yōu)點(diǎn):

    不用為所有的數(shù)據(jù)維護(hù)一個(gè)大鏈表,節(jié)省了空間占用;不用在每次數(shù)據(jù)訪問時(shí)都移動(dòng)鏈表項(xiàng),提升了緩存的性能;

但是 LRU 算法有一個(gè)問題,無法解決緩存污染問題,比如應(yīng)用一次讀取了大量的數(shù)據(jù),而這些數(shù)據(jù)只會(huì)被讀取這一次,那么這些數(shù)據(jù)會(huì)留存在 Redis 緩存中很長(zhǎng)一段時(shí)間,造成緩存污染。

因此,在 Redis 4.0 之后引入了 LFU 算法來解決這個(gè)問題。

什么是 LFU 算法?

LFU 全稱是 Least Frequently Used 翻譯為最近最不常用的,LFU 算法是根據(jù)數(shù)據(jù)訪問次數(shù)來淘汰數(shù)據(jù)的,它的核心思想是“如果數(shù)據(jù)過去被訪問多次,那么將來被訪問的頻率也更高”。

所以, LFU 算法會(huì)記錄每個(gè)數(shù)據(jù)的訪問次數(shù)。當(dāng)一個(gè)數(shù)據(jù)被再次訪問時(shí),就會(huì)增加該數(shù)據(jù)的訪問次數(shù)。這樣就解決了偶爾被訪問一次之后,數(shù)據(jù)留存在緩存中很長(zhǎng)一段時(shí)間的問題,相比于 LRU 算法也更合理一些。

Redis 是如何實(shí)現(xiàn) LFU 算法的?

LFU 算法相比于 LRU 算法的實(shí)現(xiàn),多記錄了「數(shù)據(jù)的訪問頻次」的信息。Redis 對(duì)象的結(jié)構(gòu)如下:

typedef?struct?redisObject?{
????...
??????
????//?24?bits,用于記錄對(duì)象的訪問信息
????unsigned?lru:24;??
????...
}?robj;

Redis 對(duì)象頭中的 lru 字段,在 LRU 算法下和 LFU 算法下使用方式并不相同。

在 LRU 算法中,Redis 對(duì)象頭的 24 bits 的 lru 字段是用來記錄 key 的訪問時(shí)間戳,因此在 LRU 模式下,Redis可以根據(jù)對(duì)象頭中的 lru 字段記錄的值,來比較最后一次 key 的訪問時(shí)間長(zhǎng),從而淘汰最久未被使用的 key。

在 LFU 算法中,Redis對(duì)象頭的 24 bits 的 lru 字段被分成兩段來存儲(chǔ),高 16bit 存儲(chǔ) ldt(Last Decrement Time),用來記錄 key 的訪問時(shí)間戳;低 8bit 存儲(chǔ) logc(Logistic Counter),用來記錄 key 的訪問頻次。

Redis 緩存設(shè)計(jì)

如何避免緩存雪崩、緩存擊穿、緩存穿透?

如何避免緩存雪崩?

通常我們?yōu)榱吮WC緩存中的數(shù)據(jù)與數(shù)據(jù)庫(kù)中的數(shù)據(jù)一致性,會(huì)給 Redis 里的數(shù)據(jù)設(shè)置過期時(shí)間,當(dāng)緩存數(shù)據(jù)過期后,用戶訪問的數(shù)據(jù)如果不在緩存里,業(yè)務(wù)系統(tǒng)需要重新生成緩存,因此就會(huì)訪問數(shù)據(jù)庫(kù),并將數(shù)據(jù)更新到 Redis 里,這樣后續(xù)請(qǐng)求都可以直接命中緩存。

那么,當(dāng)大量緩存數(shù)據(jù)在同一時(shí)間過期(失效)時(shí),如果此時(shí)有大量的用戶請(qǐng)求,都無法在 Redis 中處理,于是全部請(qǐng)求都直接訪問數(shù)據(jù)庫(kù),從而導(dǎo)致數(shù)據(jù)庫(kù)的壓力驟增,嚴(yán)重的會(huì)造成數(shù)據(jù)庫(kù)宕機(jī),從而形成一系列連鎖反應(yīng),造成整個(gè)系統(tǒng)崩潰,這就是緩存雪崩的問題。

對(duì)于緩存雪崩問題,我們可以采用兩種方案解決。

將緩存失效時(shí)間隨機(jī)打散:我們可以在原有的失效時(shí)間基礎(chǔ)上增加一個(gè)隨機(jī)值(比如 1 到 10 分鐘)這樣每個(gè)緩存的過期時(shí)間都不重復(fù)了,也就降低了緩存集體失效的概率。

設(shè)置緩存不過期:我們可以通過后臺(tái)服務(wù)來更新緩存數(shù)據(jù),從而避免因?yàn)榫彺媸г斐傻木彺嫜┍?,也可以在一定程度上避免緩存并發(fā)問題。

如何避免緩存擊穿?

我們的業(yè)務(wù)通常會(huì)有幾個(gè)數(shù)據(jù)會(huì)被頻繁地訪問,比如秒殺活動(dòng),這類被頻地訪問的數(shù)據(jù)被稱為熱點(diǎn)數(shù)據(jù)。

如果緩存中的某個(gè)熱點(diǎn)數(shù)據(jù)過期了,此時(shí)大量的請(qǐng)求訪問了該熱點(diǎn)數(shù)據(jù),就無法從緩存中讀取,直接訪問數(shù)據(jù)庫(kù),數(shù)據(jù)庫(kù)很容易就被高并發(fā)的請(qǐng)求沖垮,這就是緩存擊穿的問題。

可以發(fā)現(xiàn)緩存擊穿跟緩存雪崩很相似,你可以認(rèn)為緩存擊穿是緩存雪崩的一個(gè)子集。
應(yīng)對(duì)緩存擊穿可以采取前面說到兩種方案:

    互斥鎖方案(Redis 中使用 setNX 方法設(shè)置一個(gè)狀態(tài)位,表示這是一種鎖定狀態(tài)),保證同一時(shí)間只有一個(gè)業(yè)務(wù)線程請(qǐng)求緩存,未能獲取互斥鎖的請(qǐng)求,要么等待鎖釋放后重新讀取緩存,要么就返回空值或者默認(rèn)值。不給熱點(diǎn)數(shù)據(jù)設(shè)置過期時(shí)間,由后臺(tái)異步更新緩存,或者在熱點(diǎn)數(shù)據(jù)準(zhǔn)備要過期前,提前通知后臺(tái)線程更新緩存以及重新設(shè)置過期時(shí)間;

如何避免緩存穿透?

當(dāng)發(fā)生緩存雪崩或擊穿時(shí),數(shù)據(jù)庫(kù)中還是保存了應(yīng)用要訪問的數(shù)據(jù),一旦緩存恢復(fù)相對(duì)應(yīng)的數(shù)據(jù),就可以減輕數(shù)據(jù)庫(kù)的壓力,而緩存穿透就不一樣了。

當(dāng)用戶訪問的數(shù)據(jù),既不在緩存中,也不在數(shù)據(jù)庫(kù)中,導(dǎo)致請(qǐng)求在訪問緩存時(shí),發(fā)現(xiàn)緩存缺失,再去訪問數(shù)據(jù)庫(kù)時(shí),發(fā)現(xiàn)數(shù)據(jù)庫(kù)中也沒有要訪問的數(shù)據(jù),沒辦法構(gòu)建緩存數(shù)據(jù),來服務(wù)后續(xù)的請(qǐng)求。那么當(dāng)有大量這樣的請(qǐng)求到來時(shí),數(shù)據(jù)庫(kù)的壓力驟增,這就是緩存穿透的問題。

緩存穿透的發(fā)生一般有這兩種情況:

    業(yè)務(wù)誤操作,緩存中的數(shù)據(jù)和數(shù)據(jù)庫(kù)中的數(shù)據(jù)都被誤刪除了,所以導(dǎo)致緩存和數(shù)據(jù)庫(kù)中都沒有數(shù)據(jù);黑客惡意攻擊,故意大量訪問某些讀取不存在數(shù)據(jù)的業(yè)務(wù);

應(yīng)對(duì)緩存穿透的方案,常見的方案有三種。

非法請(qǐng)求的限制:當(dāng)有大量惡意請(qǐng)求訪問不存在的數(shù)據(jù)的時(shí)候,也會(huì)發(fā)生緩存穿透,因此在 API 入口處我們要判斷求請(qǐng)求參數(shù)是否合理,請(qǐng)求參數(shù)是否含有非法值、請(qǐng)求字段是否存在,如果判斷出是惡意請(qǐng)求就直接返回錯(cuò)誤,避免進(jìn)一步訪問緩存和數(shù)據(jù)庫(kù)。

設(shè)置空值或者默認(rèn)值:當(dāng)我們線上業(yè)務(wù)發(fā)現(xiàn)緩存穿透的現(xiàn)象時(shí),可以針對(duì)查詢的數(shù)據(jù),在緩存中設(shè)置一個(gè)空值或者默認(rèn)值,這樣后續(xù)請(qǐng)求就可以從緩存中讀取到空值或者默認(rèn)值,返回給應(yīng)用,而不會(huì)繼續(xù)查詢數(shù)據(jù)庫(kù)。

使用布隆過濾器快速判斷數(shù)據(jù)是否存在,避免通過查詢數(shù)據(jù)庫(kù)來判斷數(shù)據(jù)是否存在:我們可以在寫入數(shù)據(jù)庫(kù)數(shù)據(jù)時(shí),使用布隆過濾器做個(gè)標(biāo)記,然后在用戶請(qǐng)求到來時(shí),業(yè)務(wù)線程確認(rèn)緩存失效后,可以通過查詢布隆過濾器快速判斷數(shù)據(jù)是否存在,如果不存在,就不用通過查詢數(shù)據(jù)庫(kù)來判斷數(shù)據(jù)是否存在,即使發(fā)生了緩存穿透,大量請(qǐng)求只會(huì)查詢 Redis 和布隆過濾器,而不會(huì)查詢數(shù)據(jù)庫(kù),保證了數(shù)據(jù)庫(kù)能正常運(yùn)行,Redis 自身也是支持布隆過濾器的。

如何設(shè)計(jì)一個(gè)緩存策略,可以動(dòng)態(tài)緩存熱點(diǎn)數(shù)據(jù)呢?

由于數(shù)據(jù)存儲(chǔ)受限,系統(tǒng)并不是將所有數(shù)據(jù)都需要存放到緩存中的,而只是將其中一部分熱點(diǎn)數(shù)據(jù)緩存起來,所以我們要設(shè)計(jì)一個(gè)熱點(diǎn)數(shù)據(jù)動(dòng)態(tài)緩存的策略。

熱點(diǎn)數(shù)據(jù)動(dòng)態(tài)緩存的策略總體思路:通過數(shù)據(jù)最新訪問時(shí)間來做排名,并過濾掉不常訪問的數(shù)據(jù),只留下經(jīng)常訪問的數(shù)據(jù)。

以電商平臺(tái)場(chǎng)景中的例子,現(xiàn)在要求只緩存用戶經(jīng)常訪問的 Top 1000 的商品。具體細(xì)節(jié)如下:

    先通過緩存系統(tǒng)做一個(gè)排序隊(duì)列(比如存放 1000 個(gè)商品),系統(tǒng)會(huì)根據(jù)商品的訪問時(shí)間,更新隊(duì)列信息,越是最近訪問的商品排名越靠前;同時(shí)系統(tǒng)會(huì)定期過濾掉隊(duì)列中排名最后的 200 個(gè)商品,然后再?gòu)臄?shù)據(jù)庫(kù)中隨機(jī)讀取出 200 個(gè)商品加入隊(duì)列中;這樣當(dāng)請(qǐng)求每次到達(dá)的時(shí)候,會(huì)先從隊(duì)列中獲取商品 ID,如果命中,就根據(jù) ID 再?gòu)牧硪粋€(gè)緩存數(shù)據(jù)結(jié)構(gòu)中讀取實(shí)際的商品信息,并返回。

在 Redis 中可以用 zadd 方法和 zrange 方法來完成排序隊(duì)列和獲取 200 個(gè)商品的操作。

說說常見的緩存更新策略?

常見的緩存更新策略共有3種:

    Cache Aside(旁路緩存)策略;Read/Write Through(讀穿 / 寫穿)策略;Write Back(寫回)策略;

實(shí)際開發(fā)中,Redis 和 MySQL 的更新策略用的是 Cache Aside,另外兩種策略應(yīng)用不了。

Cache Aside(旁路緩存)策略

Cache Aside(旁路緩存)策略是最常用的,應(yīng)用程序直接與「數(shù)據(jù)庫(kù)、緩存」交互,并負(fù)責(zé)對(duì)緩存的維護(hù),該策略又可以細(xì)分為「讀策略」和「寫策略」。

寫策略的步驟:先更新數(shù)據(jù)庫(kù)中的數(shù)據(jù),再刪除緩存中的數(shù)據(jù)。

讀策略的步驟:如果讀取的數(shù)據(jù)命中了緩存,則直接返回?cái)?shù)據(jù);如果讀取的數(shù)據(jù)沒有命中緩存,則從數(shù)據(jù)庫(kù)中讀取數(shù)據(jù),然后將數(shù)據(jù)寫入到緩存,并且返回給用戶。

注意,寫策略的步驟的順序不能倒過來,即不能先刪除緩存再更新數(shù)據(jù)庫(kù),原因是在「讀+寫」并發(fā)的時(shí)候,會(huì)出現(xiàn)緩存和數(shù)據(jù)庫(kù)的數(shù)據(jù)不一致性的問題。

舉個(gè)例子,假設(shè)某個(gè)用戶的年齡是 20,請(qǐng)求 A 要更新用戶年齡為 21,所以它會(huì)刪除緩存中的內(nèi)容。這時(shí),另一個(gè)請(qǐng)求 B 要讀取這個(gè)用戶的年齡,它查詢緩存發(fā)現(xiàn)未命中后,會(huì)從數(shù)據(jù)庫(kù)中讀取到年齡為 20,并且寫入到緩存中,然后請(qǐng)求 A 繼續(xù)更改數(shù)據(jù)庫(kù),將用戶的年齡更新為 21。

最終,該用戶年齡在緩存中是 20(舊值),在數(shù)據(jù)庫(kù)中是 21(新值),緩存和數(shù)據(jù)庫(kù)的數(shù)據(jù)不一致。

為什么「先更新數(shù)據(jù)庫(kù)再刪除緩存」不會(huì)有數(shù)據(jù)不一致的問題?

繼續(xù)用「讀 + 寫」請(qǐng)求的并發(fā)的場(chǎng)景來分析。

假如某個(gè)用戶數(shù)據(jù)在緩存中不存在,請(qǐng)求 A 讀取數(shù)據(jù)時(shí)從數(shù)據(jù)庫(kù)中查詢到年齡為 20,在未寫入緩存中時(shí)另一個(gè)請(qǐng)求 B 更新數(shù)據(jù)。它更新數(shù)據(jù)庫(kù)中的年齡為 21,并且清空緩存。這時(shí)請(qǐng)求 A 把從數(shù)據(jù)庫(kù)中讀到的年齡為 20 的數(shù)據(jù)寫入到緩存中。

最終,該用戶年齡在緩存中是 20(舊值),在數(shù)據(jù)庫(kù)中是 21(新值),緩存和數(shù)據(jù)庫(kù)數(shù)據(jù)不一致。
從上面的理論上分析,先更新數(shù)據(jù)庫(kù),再刪除緩存也是會(huì)出現(xiàn)數(shù)據(jù)不一致性的問題,但是在實(shí)際中,這個(gè)問題出現(xiàn)的概率并不高。

因?yàn)榫彺娴膶懭胪ǔRh(yuǎn)遠(yuǎn)快于數(shù)據(jù)庫(kù)的寫入,所以在實(shí)際中很難出現(xiàn)請(qǐng)求 B 已經(jīng)更新了數(shù)據(jù)庫(kù)并且刪除了緩存,請(qǐng)求 A 才更新完緩存的情況。而一旦請(qǐng)求 A 早于請(qǐng)求 B 刪除緩存之前更新了緩存,那么接下來的請(qǐng)求就會(huì)因?yàn)榫彺娌幻卸鴱臄?shù)據(jù)庫(kù)中重新讀取數(shù)據(jù),所以不會(huì)出現(xiàn)這種不一致的情況。

Cache Aside 策略適合讀多寫少的場(chǎng)景,不適合寫多的場(chǎng)景,因?yàn)楫?dāng)寫入比較頻繁時(shí),緩存中的數(shù)據(jù)會(huì)被頻繁地清理,這樣會(huì)對(duì)緩存的命中率有一些影響。如果業(yè)務(wù)對(duì)緩存命中率有嚴(yán)格的要求,那么可以考慮兩種解決方案:

    一種做法是在更新數(shù)據(jù)時(shí)也更新緩存,只是在更新緩存前先加一個(gè)分布式鎖,因?yàn)檫@樣在同一時(shí)間只允許一個(gè)線程更新緩存,就不會(huì)產(chǎn)生并發(fā)問題了。當(dāng)然這么做對(duì)于寫入的性能會(huì)有一些影響;另一種做法同樣也是在更新數(shù)據(jù)時(shí)更新緩存,只是給緩存加一個(gè)較短的過期時(shí)間,這樣即使出現(xiàn)緩存不一致的情況,緩存的數(shù)據(jù)也會(huì)很快過期,對(duì)業(yè)務(wù)的影響也是可以接受。

Read/Write Through(讀穿 / 寫穿)策略

Read/Write Through(讀穿 / 寫穿)策略原則是應(yīng)用程序只和緩存交互,不再和數(shù)據(jù)庫(kù)交互,而是由緩存和數(shù)據(jù)庫(kù)交互,相當(dāng)于更新數(shù)據(jù)庫(kù)的操作由緩存自己代理了。

1、Read Through 策略

先查詢緩存中數(shù)據(jù)是否存在,如果存在則直接返回,如果不存在,則由緩存組件負(fù)責(zé)從數(shù)據(jù)庫(kù)查詢數(shù)據(jù),并將結(jié)果寫入到緩存組件,最后緩存組件將數(shù)據(jù)返回給應(yīng)用。

2、Write Through 策略

當(dāng)有數(shù)據(jù)更新的時(shí)候,先查詢要寫入的數(shù)據(jù)在緩存中是否已經(jīng)存在:

    如果緩存中數(shù)據(jù)已經(jīng)存在,則更新緩存中的數(shù)據(jù),并且由緩存組件同步更新到數(shù)據(jù)庫(kù)中,然后緩存組件告知應(yīng)用程序更新完成。如果緩存中數(shù)據(jù)不存在,直接更新數(shù)據(jù)庫(kù),然后返回;

下面是 Read Through/Write Through 策略的示意圖:

Read Through/Write Through 策略的特點(diǎn)是由緩存節(jié)點(diǎn)而非應(yīng)用程序來和數(shù)據(jù)庫(kù)打交道,在我們開發(fā)過程中相比 Cache Aside 策略要少見一些,原因是我們經(jīng)常使用的分布式緩存組件,無論是 Memcached 還是 Redis 都不提供寫入數(shù)據(jù)庫(kù)和自動(dòng)加載數(shù)據(jù)庫(kù)中的數(shù)據(jù)的功能。而我們?cè)谑褂帽镜鼐彺娴臅r(shí)候可以考慮使用這種策略。

Write Back(寫回)策略

Write Back(寫回)策略在更新數(shù)據(jù)的時(shí)候,只更新緩存,同時(shí)將緩存數(shù)據(jù)設(shè)置為臟的,然后立馬返回,并不會(huì)更新數(shù)據(jù)庫(kù)。對(duì)于數(shù)據(jù)庫(kù)的更新,會(huì)通過批量異步更新的方式進(jìn)行。

實(shí)際上,Write Back(寫回)策略也不能應(yīng)用到我們常用的數(shù)據(jù)庫(kù)和緩存的場(chǎng)景中,因?yàn)?Redis 并沒有異步更新數(shù)據(jù)庫(kù)的功能。

Write Back 是計(jì)算機(jī)體系結(jié)構(gòu)中的設(shè)計(jì),比如 CPU 的緩存、操作系統(tǒng)中文件系統(tǒng)的緩存都采用了 Write Back(寫回)策略。

Write Back 策略特別適合寫多的場(chǎng)景,因?yàn)榘l(fā)生寫操作的時(shí)候, 只需要更新緩存,就立馬返回了。比如,寫文件的時(shí)候,實(shí)際上是寫入到文件系統(tǒng)的緩存就返回了,并不會(huì)寫磁盤。

但是帶來的問題是,數(shù)據(jù)不是強(qiáng)一致性的,而且會(huì)有數(shù)據(jù)丟失的風(fēng)險(xiǎn),因?yàn)榫彺嬉话闶褂脙?nèi)存,而內(nèi)存是非持久化的,所以一旦緩存機(jī)器掉電,就會(huì)造成原本緩存中的臟數(shù)據(jù)丟失。所以你會(huì)發(fā)現(xiàn)系統(tǒng)在掉電之后,之前寫入的文件會(huì)有部分丟失,就是因?yàn)?Page Cache 還沒有來得及刷盤造成的。

這里貼一張 CPU 緩存與內(nèi)存使用 Write Back 策略的流程圖:

有沒有覺得這個(gè)流程很熟悉?因?yàn)槲以趯?CPU 緩存文章的時(shí)候提到過。

如何保證緩存和數(shù)據(jù)庫(kù)數(shù)據(jù)的一致性?

Redis 實(shí)戰(zhàn)

Redis 如何實(shí)現(xiàn)延遲隊(duì)列?

延遲隊(duì)列是指把當(dāng)前要做的事情,往后推遲一段時(shí)間再做。延遲隊(duì)列的常見使用場(chǎng)景有以下幾種:

    在淘寶、京東等購(gòu)物平臺(tái)上下單,超過一定時(shí)間未付款,訂單會(huì)自動(dòng)取消;打車的時(shí)候,在規(guī)定時(shí)間沒有車主接單,平臺(tái)會(huì)取消你的單并提醒你暫時(shí)沒有車主接單;點(diǎn)外賣的時(shí)候,如果商家在10分鐘還沒接單,就會(huì)自動(dòng)取消訂單;

在 Redis 可以使用有序集合(ZSet)的方式來實(shí)現(xiàn)延遲消息隊(duì)列的,ZSet 有一個(gè) Score 屬性可以用來存儲(chǔ)延遲執(zhí)行的時(shí)間。

使用 zadd score1 value1 命令就可以一直往內(nèi)存中生產(chǎn)消息。再利用 zrangebysocre 查詢符合條件的所有待處理的任務(wù), 通過循環(huán)執(zhí)行隊(duì)列任務(wù)即可。

Redis 的大 key 如何處理?

什么是 Redis 大 key?

大 key 并不是指 key 的值很大,而是 key 對(duì)應(yīng)的 value 很大。

一般而言,下面這兩種情況被稱為大 key:

    String 類型的值大于 10 KB;Hash、List、Set、ZSet 類型的元素的個(gè)數(shù)超過 5000個(gè);

大 key 會(huì)造成什么問題?

大 key 會(huì)帶來以下四種影響:

客戶端超時(shí)阻塞。由于 Redis 執(zhí)行命令是單線程處理,然后在操作大 key 時(shí)會(huì)比較耗時(shí),那么就會(huì)阻塞 Redis,從客戶端這一視角看,就是很久很久都沒有響應(yīng)。

引發(fā)網(wǎng)絡(luò)阻塞。每次獲取大 key 產(chǎn)生的網(wǎng)絡(luò)流量較大,如果一個(gè) key 的大小是 1 MB,每秒訪問量為 1000,那么每秒會(huì)產(chǎn)生 1000MB 的流量,這對(duì)于普通千兆網(wǎng)卡的服務(wù)器來說是災(zāi)難性的。

阻塞工作線程。如果使用 del 刪除大 key 時(shí),會(huì)阻塞工作線程,這樣就沒辦法處理后續(xù)的命令。

內(nèi)存分布不均。集群模型在 slot 分片均勻情況下,會(huì)出現(xiàn)數(shù)據(jù)和查詢傾斜情況,部分有大 key 的 Redis 節(jié)點(diǎn)占用內(nèi)存多,QPS 也會(huì)比較大。

如何找到大 key ?

1、redis-cli --bigkeys 查找大key

可以通過 redis-cli --bigkeys 命令查找大 key:

redis-cli?-h?127.0.0.1?-p6379?-a?"password"?--?bigkeys

使用的時(shí)候注意事項(xiàng):

    最好選擇在從節(jié)點(diǎn)上執(zhí)行該命令。因?yàn)橹鞴?jié)點(diǎn)上執(zhí)行時(shí),會(huì)阻塞主節(jié)點(diǎn);如果沒有從節(jié)點(diǎn),那么可以選擇在 Redis 實(shí)例業(yè)務(wù)壓力的低峰階段進(jìn)行掃描查詢,以免影響到實(shí)例的正常運(yùn)行;或者可以使用 -i 參數(shù)控制掃描間隔,避免長(zhǎng)時(shí)間掃描降低 Redis 實(shí)例的性能。

該方式的不足之處:

    這個(gè)方法只能返回每種類型中最大的那個(gè) bigkey,無法得到大小排在前 N 位的 bigkey;對(duì)于集合類型來說,這個(gè)方法只統(tǒng)計(jì)集合元素個(gè)數(shù)的多少,而不是實(shí)際占用的內(nèi)存量。但是,一個(gè)集合中的元素個(gè)數(shù)多,并不一定占用的內(nèi)存就多。因?yàn)?,有可能每個(gè)元素占用的內(nèi)存很小,這樣的話,即使元素個(gè)數(shù)有很多,總內(nèi)存開銷也不大;

2、使用 SCAN 命令查找大 key

使用 SCAN 命令對(duì)數(shù)據(jù)庫(kù)掃描,然后用 TYPE 命令獲取返回的每一個(gè) key 的類型。

對(duì)于 String 類型,可以直接使用 STRLEN 命令獲取字符串的長(zhǎng)度,也就是占用的內(nèi)存空間字節(jié)數(shù)。

對(duì)于集合類型來說,有兩種方法可以獲得它占用的內(nèi)存大?。?/p>

    • 如果能夠預(yù)先從業(yè)務(wù)層知道集合元素的平均大小,那么,可以使用下面的命令獲取集合元素的個(gè)數(shù),然后乘以集合元素的平均大小,這樣就能獲得集合占用的內(nèi)存大小了。List 類型:

LLEN

    • 命令;Hash 類型:

HLEN

    • 命令;Set 類型:

SCARD

    • 命令;Sorted Set 類型:

ZCARD

    • 命令;如果不能提前知道寫入集合的元素大小,可以使用

MEMORY USAGE

    命令(需要 Redis 4.0 及以上版本),查詢一個(gè)鍵值對(duì)占用的內(nèi)存空間。

3、使用 RdbTools 工具查找大 key

使用 RdbTools 第三方開源工具,可以用來解析 Redis 快照(RDB)文件,找到其中的大 key。

比如,下面這條命令,將大于 10 kb 的 ?key ?輸出到一個(gè)表格文件。

rdb?dump.rdb?-c?memory?--bytes?10240?-f?redis.csv

如何刪除大 key?

刪除操作的本質(zhì)是要釋放鍵值對(duì)占用的內(nèi)存空間,不要小瞧內(nèi)存的釋放過程。

釋放內(nèi)存只是第一步,為了更加高效地管理內(nèi)存空間,在應(yīng)用程序釋放內(nèi)存時(shí),操作系統(tǒng)需要把釋放掉的內(nèi)存塊插入一個(gè)空閑內(nèi)存塊的鏈表,以便后續(xù)進(jìn)行管理和再分配。這個(gè)過程本身需要一定時(shí)間,而且會(huì)阻塞當(dāng)前釋放內(nèi)存的應(yīng)用程序。

所以,如果一下子釋放了大量?jī)?nèi)存,空閑內(nèi)存塊鏈表操作時(shí)間就會(huì)增加,相應(yīng)地就會(huì)造成 Redis 主線程的阻塞,如果主線程發(fā)生了阻塞,其他所有請(qǐng)求可能都會(huì)超時(shí),超時(shí)越來越多,會(huì)造成 Redis 連接耗盡,產(chǎn)生各種異常。

因此,刪除大 key 這一個(gè)動(dòng)作,我們要小心。具體要怎么做呢?這里給出兩種方法:

    分批次刪除異步刪除(Redis 4.0版本以上)

1、分批次刪除

對(duì)于刪除大 Hash,使用 hscan 命令,每次獲取 100 個(gè)字段,再用 hdel 命令,每次刪除 1 個(gè)字段。

Python代碼:

def?del_large_hash():
??r?=?redis.StrictRedis(host='redis-host1',?port=6379)
????large_hash_key?="xxx"?#要?jiǎng)h除的大hash鍵名
????cursor?=?'0'
????while?cursor?!=?0:
????????#?使用?hscan?命令,每次獲取?100?個(gè)字段
????????cursor,?data?=?r.hscan(large_hash_key,?cursor=cursor,?count=100)
????????for?item?in?data.items():
????????????????#?再用?hdel?命令,每次刪除1個(gè)字段
????????????????r.hdel(large_hash_key,?item[0])

對(duì)于刪除大 List,通過 ltrim 命令,每次刪除少量元素。

Python代碼:

def?del_large_list():
??r?=?redis.StrictRedis(host='redis-host1',?port=6379)
??large_list_key?=?'xxx'??#要?jiǎng)h除的大list的鍵名
??while?r.llen(large_list_key)>0:
??????#每次只刪除最右100個(gè)元素
??????r.ltrim(large_list_key,?0,?-101)?

對(duì)于刪除大 Set,使用 sscan 命令,每次掃描集合中 100 個(gè)元素,再用 srem 命令每次刪除一個(gè)鍵。

Python代碼:

def?del_large_set():
??r?=?redis.StrictRedis(host='redis-host1',?port=6379)
??large_set_key?=?'xxx'???#?要?jiǎng)h除的大set的鍵名
??cursor?=?'0'
??while?cursor?!=?0:
????#?使用?sscan?命令,每次掃描集合中?100?個(gè)元素
????cursor,?data?=?r.sscan(large_set_key,?cursor=cursor,?count=100)
????for?item?in?data:
??????#?再用?srem?命令每次刪除一個(gè)鍵
??????r.srem(large_size_key,?item)

對(duì)于刪除大 ZSet,使用 zremrangebyrank 命令,每次刪除 top 100個(gè)元素。

Python代碼:

def?del_large_sortedset():
??r?=?redis.StrictRedis(host='large_sortedset_key',?port=6379)
??large_sortedset_key='xxx'
??while?r.zcard(large_sortedset_key)>0:
????#?使用?zremrangebyrank?命令,每次刪除?top?100個(gè)元素
????r.zremrangebyrank(large_sortedset_key,0,99)?

2、異步刪除

從 Redis 4.0 版本開始,可以采用異步刪除法,用 unlink 命令代替 del 來刪除。

這樣 Redis 會(huì)將這個(gè) key 放入到一個(gè)異步線程中進(jìn)行刪除,這樣不會(huì)阻塞主線程。

除了主動(dòng)調(diào)用 unlink 命令實(shí)現(xiàn)異步刪除之外,我們還可以通過配置參數(shù),達(dá)到某些條件的時(shí)候自動(dòng)進(jìn)行異步刪除。

主要有 4 種場(chǎng)景,默認(rèn)都是關(guān)閉的:

lazyfree-lazy-eviction?no
lazyfree-lazy-expire?no
lazyfree-lazy-server-del
noslave-lazy-flush?no

它們代表的含義如下:

lazyfree-lazy-eviction:表示當(dāng) Redis 運(yùn)行內(nèi)存超過 maxmeory 時(shí),是否開啟 lazy free 機(jī)制刪除;

lazyfree-lazy-expire:表示設(shè)置了過期時(shí)間的鍵值,當(dāng)過期之后是否開啟 lazy free 機(jī)制刪除;

lazyfree-lazy-server-del:有些指令在處理已存在的鍵時(shí),會(huì)帶有一個(gè)隱式的 del 鍵的操作,比如 rename 命令,當(dāng)目標(biāo)鍵已存在,Redis 會(huì)先刪除目標(biāo)鍵,如果這些目標(biāo)鍵是一個(gè) big key,就會(huì)造成阻塞刪除的問題,此配置表示在這種場(chǎng)景中是否開啟 lazy free 機(jī)制刪除;

slave-lazy-flush:針對(duì) slave (從節(jié)點(diǎn)) 進(jìn)行全量數(shù)據(jù)同步,slave 在加載 master 的 RDB 文件前,會(huì)運(yùn)行 flushall 來清理自己的數(shù)據(jù),它表示此時(shí)是否開啟 lazy free 機(jī)制刪除。

建議開啟其中的 lazyfree-lazy-eviction、lazyfree-lazy-expire、lazyfree-lazy-server-del 等配置,這樣就可以有效的提高主線程的執(zhí)行效率。

Redis 管道有什么用?

管道技術(shù)(Pipeline)是客戶端提供的一種批處理技術(shù),用于一次處理多個(gè) Redis 命令,從而提高整個(gè)交互的性能。

普通命令模式,如下圖所示:

管道模式,如下圖所示:

使用管道技術(shù)可以解決多個(gè)命令執(zhí)行時(shí)的網(wǎng)絡(luò)等待,它是把多個(gè)命令整合到一起發(fā)送給服務(wù)器端處理之后統(tǒng)一返回給客戶端,這樣就免去了每條命令執(zhí)行后都要等待的情況,從而有效地提高了程序的執(zhí)行效率。

但使用管道技術(shù)也要注意避免發(fā)送的命令過大,或管道內(nèi)的數(shù)據(jù)太多而導(dǎo)致的網(wǎng)絡(luò)阻塞。

要注意的是,管道技術(shù)本質(zhì)上是客戶端提供的功能,而非 Redis 服務(wù)器端的功能。

Redis 事務(wù)支持回滾嗎?

MySQL 在執(zhí)行事務(wù)時(shí),會(huì)提供回滾機(jī)制,當(dāng)事務(wù)執(zhí)行發(fā)生錯(cuò)誤時(shí),事務(wù)中的所有操作都會(huì)撤銷,已經(jīng)修改的數(shù)據(jù)也會(huì)被恢復(fù)到事務(wù)執(zhí)行前的狀態(tài)。

Redis 中并沒有提供回滾機(jī)制,雖然 Redis 提供了 DISCARD 命令,但是這個(gè)命令只能用來主動(dòng)放棄事務(wù)執(zhí)行,把暫存的命令隊(duì)列清空,起不到回滾的效果。

下面是 DISCARD 命令用法:

#讀取?count?的值4
127.0.0.1:6379>?GET?count
"1"
#開啟事務(wù)
127.0.0.1:6379>?MULTI?
OK
#發(fā)送事務(wù)的第一個(gè)操作,對(duì)count減1
127.0.0.1:6379>?DECR?count
QUEUED
#執(zhí)行DISCARD命令,主動(dòng)放棄事務(wù)
127.0.0.1:6379>?DISCARD
OK
#再次讀取a:stock的值,值沒有被修改
127.0.0.1:6379>?GET?count
"1"

事務(wù)執(zhí)行過程中,如果命令入隊(duì)時(shí)沒報(bào)錯(cuò),而事務(wù)提交后,實(shí)際執(zhí)行時(shí)報(bào)錯(cuò)了,正確的命令依然可以正常執(zhí)行,所以這可以看出 Redis 并不一定保證原子性(原子性:事務(wù)中的命令要不全部成功,要不全部失?。?。

比如下面這個(gè)例子:

#獲取name原本的值
127.0.0.1:6379>?GET?name
"xiaolin"
#開啟事務(wù)
127.0.0.1:6379>?MULTI
OK
#設(shè)置新值
127.0.0.1:6379(TX)>?SET?name?xialincoding
QUEUED
#注意,這條命令是錯(cuò)誤的
#?expire?過期時(shí)間正確來說是數(shù)字,并不是‘10s’字符串,但是還是入隊(duì)成功了
127.0.0.1:6379(TX)>?EXPIRE?name?10s
QUEUED
#提交事務(wù),執(zhí)行報(bào)錯(cuò)
#可以看到?set?執(zhí)行成功,而?expire?執(zhí)行錯(cuò)誤。
127.0.0.1:6379(TX)>?EXEC
1)?OK
2)?(error)?ERR?value?is?not?an?integer?or?out?of?range
#可以看到,name?還是被設(shè)置為新值了
127.0.0.1:6379>?GET?name
"xialincoding"

為什么Redis 不支持事務(wù)回滾?

Redis 官方文檔的解釋如下:

大概的意思是,作者不支持事務(wù)回滾的原因有以下兩個(gè):

    他認(rèn)為 Redis 事務(wù)的執(zhí)行時(shí),錯(cuò)誤通常都是編程錯(cuò)誤造成的,這種錯(cuò)誤通常只會(huì)出現(xiàn)在開發(fā)環(huán)境中,而很少會(huì)在實(shí)際的生產(chǎn)環(huán)境中出現(xiàn),所以他認(rèn)為沒有必要為 Redis 開發(fā)事務(wù)回滾功能;不支持事務(wù)回滾是因?yàn)檫@種復(fù)雜的功能和 Redis 追求的簡(jiǎn)單高效的設(shè)計(jì)主旨不符合。

這里不支持事務(wù)回滾,指的是不支持事務(wù)運(yùn)行時(shí)錯(cuò)誤的事務(wù)回滾。

如何用 Redis 實(shí)現(xiàn)分布式鎖的?

分布式鎖是用于分布式環(huán)境下并發(fā)控制的一種機(jī)制,用于控制某個(gè)資源在同一時(shí)刻只能被一個(gè)應(yīng)用所使用。如下圖所示:

Redis 本身可以被多個(gè)客戶端共享訪問,正好就是一個(gè)共享存儲(chǔ)系統(tǒng),可以用來保存分布式鎖,而且 Redis 的讀寫性能高,可以應(yīng)對(duì)高并發(fā)的鎖操作場(chǎng)景。

Redis 的 SET 命令有個(gè) NX 參數(shù)可以實(shí)現(xiàn)「key不存在才插入」,所以可以用它來實(shí)現(xiàn)分布式鎖:

    如果 key 不存在,則顯示插入成功,可以用來表示加鎖成功;如果 key 存在,則會(huì)顯示插入失敗,可以用來表示加鎖失敗。

基于 Redis 節(jié)點(diǎn)實(shí)現(xiàn)分布式鎖時(shí),對(duì)于加鎖操作,我們需要滿足三個(gè)條件。

    加鎖包括了讀取鎖變量、檢查鎖變量值和設(shè)置鎖變量值三個(gè)操作,但需要以原子操作的方式完成,所以,我們使用 SET 命令帶上 NX 選項(xiàng)來實(shí)現(xiàn)加鎖;鎖變量需要設(shè)置過期時(shí)間,以免客戶端拿到鎖后發(fā)生異常,導(dǎo)致鎖一直無法釋放,所以,我們?cè)?SET 命令執(zhí)行時(shí)加上 EX/PX 選項(xiàng),設(shè)置其過期時(shí)間;鎖變量的值需要能區(qū)分來自不同客戶端的加鎖操作,以免在釋放鎖時(shí),出現(xiàn)誤釋放操作,所以,我們使用 SET 命令設(shè)置鎖變量值時(shí),每個(gè)客戶端設(shè)置的值是一個(gè)唯一值,用于標(biāo)識(shí)客戶端;

滿足這三個(gè)條件的分布式命令如下:

SET?lock_key?unique_value?NX?PX?10000?
    lock_key 就是 key 鍵;unique_value 是客戶端生成的唯一的標(biāo)識(shí),區(qū)分來自不同客戶端的鎖操作;NX 代表只在 lock_key 不存在時(shí),才對(duì) lock_key 進(jìn)行設(shè)置操作;PX 10000 表示設(shè)置 lock_key 的過期時(shí)間為 10s,這是為了避免客戶端發(fā)生異常而無法釋放鎖。

而解鎖的過程就是將 lock_key 鍵刪除(del lock_key),但不能亂刪,要保證執(zhí)行操作的客戶端就是加鎖的客戶端。所以,解鎖的時(shí)候,我們要先判斷鎖的 unique_value 是否為加鎖客戶端,是的話,才將 lock_key 鍵刪除。

可以看到,解鎖是有兩個(gè)操作,這時(shí)就需要 Lua 腳本來保證解鎖的原子性,因?yàn)?Redis 在執(zhí)行 Lua 腳本時(shí),可以以原子性的方式執(zhí)行,保證了鎖釋放操作的原子性。

//?釋放鎖時(shí),先比較?unique_value?是否相等,避免鎖的誤釋放
if?redis.call("get",KEYS[1])?==?ARGV[1]?then
????return?redis.call("del",KEYS[1])
else
????return?0
end

這樣一來,就通過使用 SET 命令和 Lua 腳本在 Redis 單節(jié)點(diǎn)上完成了分布式鎖的加鎖和解鎖。

基于 Redis 實(shí)現(xiàn)分布式鎖有什么優(yōu)缺點(diǎn)?

基于 Redis 實(shí)現(xiàn)分布式鎖的優(yōu)點(diǎn)

    性能高效(這是選擇緩存實(shí)現(xiàn)分布式鎖最核心的出發(fā)點(diǎn))。實(shí)現(xiàn)方便。很多研發(fā)工程師選擇使用 Redis 來實(shí)現(xiàn)分布式鎖,很大成分上是因?yàn)?Redis 提供了 setnx 方法,實(shí)現(xiàn)分布式鎖很方便。避免單點(diǎn)故障(因?yàn)?Redis 是跨集群部署的,自然就避免了單點(diǎn)故障)。

基于 Redis 實(shí)現(xiàn)分布式鎖的缺點(diǎn)

超時(shí)時(shí)間不好設(shè)置

    • 。如果鎖的超時(shí)時(shí)間設(shè)置過長(zhǎng),會(huì)影響性能,如果設(shè)置的超時(shí)時(shí)間過短會(huì)保護(hù)不到共享資源。比如在有些場(chǎng)景中,一個(gè)線程 A 獲取到了鎖之后,由于業(yè)務(wù)代碼執(zhí)行時(shí)間可能比較長(zhǎng),導(dǎo)致超過了鎖的超時(shí)時(shí)間,自動(dòng)失效,注意 A 線程沒執(zhí)行完,后續(xù)線程 B 又意外的持有了鎖,意味著可以操作共享資源,那么兩個(gè)線程之間的共享資源就沒辦法進(jìn)行保護(hù)了。

那么如何合理設(shè)置超時(shí)時(shí)間呢?

      • 我們可以基于續(xù)約的方式設(shè)置超時(shí)時(shí)間:先給鎖設(shè)置一個(gè)超時(shí)時(shí)間,然后啟動(dòng)一個(gè)守護(hù)線程,讓守護(hù)線程在一段時(shí)間后,重新設(shè)置這個(gè)鎖的超時(shí)時(shí)間。實(shí)現(xiàn)方式就是:寫一個(gè)守護(hù)線程,然后去判斷鎖的情況,當(dāng)鎖快失效的時(shí)候,再次進(jìn)行續(xù)約加鎖,當(dāng)主線程執(zhí)行完成后,銷毀續(xù)約鎖即可,不過這種方式實(shí)現(xiàn)起來相對(duì)復(fù)雜。

Redis 主從復(fù)制模式中的數(shù)據(jù)是異步復(fù)制的,這樣導(dǎo)致分布式鎖的不可靠性。如果在 Redis 主節(jié)點(diǎn)獲取到鎖后,在沒有同步到其他節(jié)點(diǎn)時(shí),Redis 主節(jié)點(diǎn)宕機(jī)了,此時(shí)新的 Redis 主節(jié)點(diǎn)依然可以獲取鎖,所以多個(gè)應(yīng)用服務(wù)就可以同時(shí)獲取到鎖。

Redis 如何解決集群情況下分布式鎖的可靠性?

為了保證集群環(huán)境下分布式鎖的可靠性,Redis 官方已經(jīng)設(shè)計(jì)了一個(gè)分布式鎖算法 Redlock(紅鎖)。

它是基于多個(gè) Redis 節(jié)點(diǎn)的分布式鎖,即使有節(jié)點(diǎn)發(fā)生了故障,鎖變量仍然是存在的,客戶端還是可以完成鎖操作。官方推薦是至少部署 ?5 個(gè) Redis 節(jié)點(diǎn),而且都是主節(jié)點(diǎn),它們之間沒有任何關(guān)系,都是一個(gè)個(gè)孤立的節(jié)點(diǎn)。

Redlock 算法的基本思路,是讓客戶端和多個(gè)獨(dú)立的 Redis 節(jié)點(diǎn)依次請(qǐng)求申請(qǐng)加鎖,如果客戶端能夠和半數(shù)以上的節(jié)點(diǎn)成功地完成加鎖操作,那么我們就認(rèn)為,客戶端成功地獲得分布式鎖,否則加鎖失敗。

這樣一來,即使有某個(gè) Redis 節(jié)點(diǎn)發(fā)生故障,因?yàn)殒i的數(shù)據(jù)在其他節(jié)點(diǎn)上也有保存,所以客戶端仍然可以正常地進(jìn)行鎖操作,鎖的數(shù)據(jù)也不會(huì)丟失。

Redlock 算法加鎖三個(gè)過程:

    • 第一步是,客戶端獲取當(dāng)前時(shí)間(t1)。第二步是,客戶端按順序依次向 N 個(gè) Redis 節(jié)點(diǎn)執(zhí)行加鎖操作:

      • 加鎖操作使用 SET 命令,帶上 NX,EX/PX 選項(xiàng),以及帶上客戶端的唯一標(biāo)識(shí)。如果某個(gè) Redis 節(jié)點(diǎn)發(fā)生故障了,為了保證在這種情況下,Redlock 算法能夠繼續(xù)運(yùn)行,我們需要給「加鎖操作」設(shè)置一個(gè)超時(shí)時(shí)間(不是對(duì)「鎖」設(shè)置超時(shí)時(shí)間,而是對(duì)「加鎖操作」設(shè)置超時(shí)時(shí)間),加鎖操作的超時(shí)時(shí)間需要遠(yuǎn)遠(yuǎn)地小于鎖的過期時(shí)間,一般也就是設(shè)置為幾十毫秒。

第三步是,一旦客戶端從超過半數(shù)(大于等于 N/2+1)的 Redis 節(jié)點(diǎn)上成功獲取到了鎖,就再次獲取當(dāng)前時(shí)間(t2),然后計(jì)算計(jì)算整個(gè)加鎖過程的總耗時(shí)(t2-t1)。如果 t2-t1 < 鎖的過期時(shí)間,此時(shí),認(rèn)為客戶端加鎖成功,否則認(rèn)為加鎖失敗。

可以看到,加鎖成功要同時(shí)滿足兩個(gè)條件(簡(jiǎn)述:如果有超過半數(shù)的 Redis 節(jié)點(diǎn)成功的獲取到了鎖,并且總耗時(shí)沒有超過鎖的有效時(shí)間,那么就是加鎖成功):

    條件一:客戶端從超過半數(shù)(大于等于 N/2+1)的 Redis 節(jié)點(diǎn)上成功獲取到了鎖;條件二:客戶端從大多數(shù)節(jié)點(diǎn)獲取鎖的總耗時(shí)(t2-t1)小于鎖設(shè)置的過期時(shí)間。

加鎖成功后,客戶端需要重新計(jì)算這把鎖的有效時(shí)間,計(jì)算的結(jié)果是「鎖最初設(shè)置的過期時(shí)間」減去「客戶端從大多數(shù)節(jié)點(diǎn)獲取鎖的總耗時(shí)(t2-t1)」。如果計(jì)算的結(jié)果已經(jīng)來不及完成共享數(shù)據(jù)的操作了,我們可以釋放鎖,以免出現(xiàn)還沒完成數(shù)據(jù)操作,鎖就過期了的情況。

加鎖失敗后,客戶端向所有 Redis 節(jié)點(diǎn)發(fā)起釋放鎖的操作,釋放鎖的操作和在單節(jié)點(diǎn)上釋放鎖的操作一樣,只要執(zhí)行釋放鎖的 Lua 腳本就可以了。

參考資料:

    《Redis 設(shè)計(jì)與實(shí)現(xiàn)》《Redis 實(shí)戰(zhàn)》《Redis 核心技術(shù)與實(shí)戰(zhàn)》《Redis 核心原理與實(shí)戰(zhàn) 》

 

推薦器件

更多器件
器件型號(hào) 數(shù)量 器件廠商 器件描述 數(shù)據(jù)手冊(cè) ECAD模型 風(fēng)險(xiǎn)等級(jí) 參考價(jià)格 更多信息
KSZ8873MLLI 1 Microchip Technology Inc DATACOM, LAN SWITCHING CIRCUIT, PQFP64
$6.1 查看
NX3215SA-32.768K-STD-MUA-8 1 Nihon Dempa Kogyo Co Ltd Parallel - Fundamental Quartz Crystal, 0.032768MHz Nom, ROHS COMPLIANT PACKAGE-2
$1.98 查看
ECS-120-20-30B-DU-TR 1 ECS International Inc Parallel - Fundamental Quartz Crystal, 12MHz Nom, SMD, 4 PIN
$11.35 查看

相關(guān)推薦

電子產(chǎn)業(yè)圖譜