大家好,我是小林。
不知不覺《圖解 Redis》系列文章寫了很多了,考慮到一些同學(xué)面試突擊 Redis,所以我整理了 3 萬字 + 40 張圖的 Redis 八股文,共收集了 40 多個(gè)面試題。
![d49af692-157a-11ee-962d-dac502259ad0.jpg](https://file1.elecfans.com//web2/M00/9D/FD/wKgaomTn_I2APp6SAAAWl5mNq-E184.jpg)
發(fā)車!
![d4b123b8-157a-11ee-962d-dac502259ad0.png](https://file1.elecfans.com//web2/M00/9D/FD/wKgaomTn_I2AMyjlAARnj6Tw8eo716.png)
認(rèn)識(shí) Redis
什么是 Redis?
我們直接看 Redis 官方是怎么介紹自己的。
![d4ebd9f4-157a-11ee-962d-dac502259ad0.jpg](https://file1.elecfans.com//web2/M00/9D/FD/wKgaomTn_I2AG5YGAAHyMjoX_sg867.jpg)
Redis 官方的介紹原版是英文的,我翻譯成了中文后截圖的,所以有些文字讀起來會(huì)比較拗口,沒關(guān)系,我會(huì)把里面比較重要的特性抽出來講一下。
Redis 是一種基于內(nèi)存的數(shù)據(jù)庫(kù),對(duì)數(shù)據(jù)的讀寫操作都是在內(nèi)存中完成,因此讀寫速度非常快,常用于緩存,消息隊(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)快。
![d518bbe0-157a-11ee-962d-dac502259ad0.png](https://file1.elecfans.com//web2/M00/9D/FE/wKgaomTn_I2ADwohAACznl5WXj8591.png)
如果 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(有序集合)。
![d561abac-157a-11ee-962d-dac502259ad0.png](https://file1.elecfans.com//web2/M00/9D/FE/wKgaomTn_I6AFlgyAAE0wSa6QB4050.png)
![d5711308-157a-11ee-962d-dac502259ad0.png](https://file1.elecfans.com//web2/M00/9D/FE/wKgaomTn_I6ARmKJAALzTGWhUOw667.png)
隨著 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)頁 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 版本的。
![d5994814-157a-11ee-962d-dac502259ad0.png](https://file1.elecfans.com//web2/M00/9D/FE/wKgaomTn_I6AYfzqAAHGixTdWP4123.png)
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à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 語言的字符串并不記錄自身長(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)的方法即可。
![d5bb40ea-157a-11ee-962d-dac502259ad0.png](https://file1.elecfans.com//web2/M00/9D/FE/wKgaomTn_I6AWs_aAAIXaNEOjts788.png)
關(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 版本之前的單線模式如下圖:
![d5e8724a-157a-11ee-962d-dac502259ad0.png](https://file1.elecfans.com//web2/M00/9D/FE/wKgaomTn_I6AQ9EwAAOo6lwr5pU471.png)
圖中的藍(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/每秒,如下圖所示:
![d600fea0-157a-11ee-962d-dac502259ad0.png](https://file1.elecfans.com//web2/M00/9D/FE/wKgaomTn_I6AOQ9ZAACYb1qlvl0334.png)
之所以 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。
![d62149da-157a-11ee-962d-dac502259ad0.png](https://file1.elecfans.com//web2/M00/9D/FE/wKgaomTn_I-AcjuxAAOsXRL8f14337.png)
核心意思是: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-readsyes
同時(shí), Redis.conf 配置文件中提供了 IO 多線程個(gè)數(shù)的配置項(xiàng)。
//io-threadsN,表示啟用N-1個(gè)I/O多線程(主線程也算一個(gè)I/O線程)
io-threads4
關(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ù)。
![d67ba74a-157a-11ee-962d-dac502259ad0.png](https://file1.elecfans.com//web2/M00/9D/FE/wKgaomTn_I-AAWtCAACPJJYzqOg375.png)
我這里以「_set name xiaolin_」命令作為例子,Redis 執(zhí)行了這條命令后,記錄在 AOF 日志里的內(nèi)容如下圖:
![d69f6d9c-157a-11ee-962d-dac502259ad0.png](https://file1.elecfans.com//web2/M00/9D/FE/wKgaomTn_I-ANbWIAACDygRv9J8833.png)
我這里給大家解釋下。
「*3」表示當(dāng)前命令有三個(gè)部分,每部分都是以「3 set」表示這部分有 3 個(gè)字節(jié),也就是「set」命令這個(gè)字符串的長(zhǎng)度。
為什么先執(zhí)行命令,再把數(shù)據(jù)寫入日志呢?
Reids 是先執(zhí)行寫操作命令后,才將該命令記錄到 AOF 日志里的,這么做其實(shí)有兩個(gè)好處。
- 避免額外的檢查開銷:因?yàn)槿绻葘懖僮髅钣涗浀?AOF 日志里,再執(zhí)行該命令的話,如果當(dāng)前的命令語法有問題,那么如果不進(jìn)行命令語法檢查,該錯(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 日志的過程,如下圖:
![d6ae78b4-157a-11ee-962d-dac502259ad0.png](https://file1.elecfans.com//web2/M00/9D/FE/wKgaomTn_I-AfTfHAADsefc6x_k922.png)
具體說說:
- 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é)成了一張表格:
![d6d7e97e-157a-11ee-962d-dac502259ad0.png](https://file1.elecfans.com//web2/M00/9D/FE/wKgaomTn_I-AJ37yAAEdCCqiTio938.png)
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 文件。
![d6fb4748-157a-11ee-962d-dac502259ad0.png](https://file1.elecfans.com//web2/M00/9D/FE/wKgaomTn_I-AYLr4AAD-ooFwPNg410.png)
但是在使用重寫機(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ū)」。
![d7232b0a-157a-11ee-962d-dac502259ad0.png](https://file1.elecfans.com//web2/M00/9D/FE/wKgaomTn_I-AdwwzAAGa_KRRJP8462.png)
也就是說,在 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ì)提供以下配置:
save9001
save30010
save6010000
別看選項(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)程的頁表,但是頁表指向的物理內(nèi)存還是一個(gè),此時(shí)如果主線程執(zhí)行讀操作,則主線程和 bgsave 子進(jìn)程互相不影響。
![d72bf10e-157a-11ee-962d-dac502259ad0.png](https://file1.elecfans.com//web2/M00/9D/FE/wKgaomTn_JCAOM5PAACkYaG2UJk510.png)
如果主線程執(zhí)行寫操作,則被修改的數(shù)據(jù)會(huì)復(fù)制一份副本,然后 bgsave 子進(jìn)程會(huì)把該副本數(shù)據(jù)寫入 RDB 文件,在這個(gè)過程中,主線程仍然可以直接修改原來的數(shù)據(jù)。
![d753eed4-157a-11ee-962d-dac502259ad0.png](https://file1.elecfans.com//web2/M00/9D/FE/wKgaomTn_JCAEFIPAACmvgTERwA471.png)
::: 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ù)。
![d776fb5e-157a-11ee-962d-dac502259ad0.jpg](https://file1.elecfans.com//web2/M00/9D/FE/wKgaomTn_JCAFqU_AAAoY9iYWIE374.jpg)
這樣的好處在于,重啟 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í)行這條命令。
![d79b14e4-157a-11ee-962d-dac502259ad0.png](https://file1.elecfans.com//web2/M00/9D/FE/wKgaomTn_JCASHVEAACmhmi9FX4863.png)
也就是說,所有的數(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)移的功能。
![d7b7abe0-157a-11ee-962d-dac502259ad0.png](https://file1.elecfans.com//web2/M00/9D/FE/wKgaomTn_JCAPmn2AADam3UeteE332.png)
::: tip
想更詳細(xì)了解 Redis 哨兵的工作原理,可以詳細(xì)看這篇:哨兵是怎么實(shí)現(xià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)系。
![d7e5bbf2-157a-11ee-962d-dac502259ad0.png](https://file1.elecfans.com//web2/M00/9D/FE/wKgaomTn_JCASlyxAACwkP31VFY495.png)
上圖中的切片集群一共有 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-h192.168.1.10–p6379clusteraddslots0,1
redis-cli-h192.168.1.11–p6379clusteraddslots2,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。
惰性刪除的流程圖如下:
![d8063288-157a-11ee-962d-dac502259ad0.png](https://file1.elecfans.com//web2/M00/9D/FE/wKgaomTn_JCAFdO3AAEm7Bvr5_g330.png)
惰性刪除策略的優(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。
定期刪除的流程如下:
![d838ee58-157a-11ee-962d-dac502259ad0.png](https://file1.elecfans.com//web2/M00/9D/FE/wKgaomTn_JCATN3yAAHIixgzAeo646.png)
定期刪除策略的優(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)之間取得平衡。
::: tip
Redis 的過期刪除的內(nèi)容就暫時(shí)提這些,想更詳細(xì)了解的,可以詳細(xì)看這篇:Redis 過期刪除策略和內(nèi)存淘汰策略有什么區(qū)別?
:::
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)如下:
typedefstructredisObject{
...
//24bits,用于記錄對(duì)象的訪問信息
unsignedlru: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 的訪問頻次。
![d8669eca-157a-11ee-962d-dac502259ad0.png](https://file1.elecfans.com//web2/M00/9D/FE/wKgaomTn_JGAJRC_AABaBacey7Q246.png)
::: tip
Redis 的內(nèi)存淘汰的內(nèi)容就暫時(shí)提這些,想更詳細(xì)了解的,可以詳細(xì)看這篇:Redis 過期刪除策略和內(nèi)存淘汰策略有什么區(qū)別?
:::
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)求都可以直接命中緩存。
![d8847030-157a-11ee-962d-dac502259ad0.png](https://file1.elecfans.com//web2/M00/9D/FE/wKgaomTn_JGAcefqAACm048dAD4178.png)
那么,當(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)榫彺媸г斐傻木彺嫜┍溃部梢栽谝欢ǔ潭壬媳苊饩彺娌l(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)求沖垮,這就是緩存擊穿的問題。
![d8ad8e84-157a-11ee-962d-dac502259ad0.png](https://file1.elecfans.com//web2/M00/9D/FE/wKgaomTn_JGAQYPoAAE9mkc_lEA871.png)
可以發(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ù)的壓力驟增,這就是緩存穿透的問題。
![d8e522e0-157a-11ee-962d-dac502259ad0.png](https://file1.elecfans.com//web2/M00/9D/FE/wKgaomTn_JGAEaD7AAErqWA15Ds753.png)
緩存穿透的發(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 自身也是支持布隆過濾器的。
::: tip
推薦閱讀:什么是緩存雪崩、擊穿、穿透?
:::
如何設(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ì)分為「讀策略」和「寫策略」。
![d93ff71a-157a-11ee-962d-dac502259ad0.png](https://file1.elecfans.com//web2/M00/9D/FE/wKgaomTn_JGAGUtLAACwPi70B4M779.png)
寫策略的步驟:
- 先更新數(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。
![d95a88be-157a-11ee-962d-dac502259ad0.png](https://file1.elecfans.com//web2/M00/9D/FE/wKgaomTn_JGAD-8rAADFk2CHHcM310.png)
最終,該用戶年齡在緩存中是 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ù)寫入到緩存中。
![d9790000-157a-11ee-962d-dac502259ad0.png](https://file1.elecfans.com//web2/M00/9D/FE/wKgaomTn_JGAR4rkAAC6jkJhQ30481.png)
最終,該用戶年齡在緩存中是 20(舊值),在數(shù)據(jù)庫(kù)中是 21(新值),緩存和數(shù)據(jù)庫(kù)數(shù)據(jù)不一致。從上面的理論上分析,先更新數(shù)據(jù)庫(kù),再刪除緩存也是會(huì)出現(xiàn)數(shù)據(jù)不一致性的問題,但是在實(shí)際中,這個(gè)問題出現(xiàn)的概率并不高。
因?yàn)榫彺娴膶懭胪ǔRh(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 策略的示意圖:
![d98c68b6-157a-11ee-962d-dac502259ad0.png](https://file1.elecfans.com//web2/M00/9D/FE/wKgaomTn_JKAfwZ_AAHzCBJkK4Y402.png)
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 策略的流程圖:
![d9bdbf92-157a-11ee-962d-dac502259ad0.png](https://file1.elecfans.com//web2/M00/9D/FE/wKgaomTn_JKAFOZGAAD28dlBC1E629.png)
有沒有覺得這個(gè)流程很熟悉?因?yàn)槲以趯?CPU 緩存文章的時(shí)候提到過。
如何保證緩存和數(shù)據(jù)庫(kù)數(shù)據(jù)的一致性?
::: tip
推薦閱讀:數(shù)據(jù)庫(kù)和緩存如何保證一致性?
:::
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ù)即可。
![d9e87eee-157a-11ee-962d-dac502259ad0.png](https://file1.elecfans.com//web2/M00/9D/FE/wKgaomTn_JKAa71UAABmuNTKGd8598.png)
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-h127.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)存大小:
-
如果能夠預(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è)表格文件。
rdbdump.rdb-cmemory--bytes10240-fredis.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代碼:
defdel_large_hash():
r=redis.StrictRedis(host='redis-host1',port=6379)
large_hash_key="xxx"#要?jiǎng)h除的大hash鍵名
cursor='0'
whilecursor!=0:
#使用hscan命令,每次獲取100個(gè)字段
cursor,data=r.hscan(large_hash_key,cursor=cursor,count=100)
foritemindata.items():
#再用hdel命令,每次刪除1個(gè)字段
r.hdel(large_hash_key,item[0])
對(duì)于刪除大 List,通過 ltrim
命令,每次刪除少量元素。
Python代碼:
defdel_large_list():
r=redis.StrictRedis(host='redis-host1',port=6379)
large_list_key='xxx'#要?jiǎng)h除的大list的鍵名
whiler.llen(large_list_key)>0:
#每次只刪除最右100個(gè)元素
r.ltrim(large_list_key,0,-101)
對(duì)于刪除大 Set,使用 sscan
命令,每次掃描集合中 100 個(gè)元素,再用 srem
命令每次刪除一個(gè)鍵。
Python代碼:
defdel_large_set():
r=redis.StrictRedis(host='redis-host1',port=6379)
large_set_key='xxx'#要?jiǎng)h除的大set的鍵名
cursor='0'
whilecursor!=0:
#使用sscan命令,每次掃描集合中100個(gè)元素
cursor,data=r.sscan(large_set_key,cursor=cursor,count=100)
foritemindata:
#再用srem命令每次刪除一個(gè)鍵
r.srem(large_size_key,item)
對(duì)于刪除大 ZSet,使用 zremrangebyrank
命令,每次刪除 top 100個(gè)元素。
Python代碼:
defdel_large_sortedset():
r=redis.StrictRedis(host='large_sortedset_key',port=6379)
large_sortedset_key='xxx'
whiler.zcard(large_sortedset_key)>0:
#使用zremrangebyrank命令,每次刪除top100個(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-evictionno
lazyfree-lazy-expireno
lazyfree-lazy-server-del
noslave-lazy-flushno
它們代表的含義如下:
-
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è)交互的性能。
普通命令模式,如下圖所示:
![da078ae6-157a-11ee-962d-dac502259ad0.png](https://file1.elecfans.com//web2/M00/9D/FE/wKgaomTn_JKAQmdzAABDt_mvyEE178.png)
管道模式,如下圖所示:
![da2ca0a6-157a-11ee-962d-dac502259ad0.png](https://file1.elecfans.com//web2/M00/9D/FE/wKgaomTn_JKAP1FGAABA7A7GubM624.png)
使用管道技術(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>GETcount
"1"
#開啟事務(wù)
127.0.0.1:6379>MULTI
OK
#發(fā)送事務(wù)的第一個(gè)操作,對(duì)count減1
127.0.0.1:6379>DECRcount
QUEUED
#執(zhí)行DISCARD命令,主動(dòng)放棄事務(wù)
127.0.0.1:6379>DISCARD
OK
#再次讀取a:stock的值,值沒有被修改
127.0.0.1:6379>GETcount
"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>GETname
"xiaolin"
#開啟事務(wù)
127.0.0.1:6379>MULTI
OK
#設(shè)置新值
127.0.0.1:6379(TX)>SETnamexialincoding
QUEUED
#注意,這條命令是錯(cuò)誤的
#expire過期時(shí)間正確來說是數(shù)字,并不是‘10s’字符串,但是還是入隊(duì)成功了
127.0.0.1:6379(TX)>EXPIREname10s
QUEUED
#提交事務(wù),執(zhí)行報(bào)錯(cuò)
#可以看到set執(zhí)行成功,而expire執(zhí)行錯(cuò)誤。
127.0.0.1:6379(TX)>EXEC
1)OK
2)(error)ERRvalueisnotanintegeroroutofrange
#可以看到,name還是被設(shè)置為新值了
127.0.0.1:6379>GETname
"xialincoding"
為什么Redis 不支持事務(wù)回滾?
Redis 官方文檔的解釋如下:
![da59685c-157a-11ee-962d-dac502259ad0.png](https://file1.elecfans.com//web2/M00/9D/FE/wKgaomTn_JKAS94DAAYMVw7kG7k207.png)
大概的意思是,作者不支持事務(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)用所使用。如下圖所示:
![dabc4c1a-157a-11ee-962d-dac502259ad0.png](https://file1.elecfans.com//web2/M00/9D/FE/wKgaomTn_JKAfd2YAAEW6xCrOmY835.png)
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è)條件的分布式命令如下:
SETlock_keyunique_valueNXPX10000
- 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是否相等,避免鎖的誤釋放
ifredis.call("get",KEYS[1])==ARGV[1]then
returnredis.call("del",KEYS[1])
else
return0
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 腳本就可以了。
-
MySQL
+關(guān)注
關(guān)注
1文章
829瀏覽量
26741 -
數(shù)據(jù)類型
+關(guān)注
關(guān)注
0文章
236瀏覽量
13661 -
Redis
+關(guān)注
關(guān)注
0文章
378瀏覽量
10937
原文標(biāo)題:3 萬字 + 40 張圖 | Redis 常見面試題(2023 版本)
文章出處:【微信號(hào):小林coding,微信公眾號(hào):小林coding】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
常見的嵌入式C語言面試題
C/C++程序員應(yīng)聘常見面試題深入解析
視頻教程:Java常見面試題目深度解析!
視頻教程:Java常見面試題目深度解析!
單片機(jī)工程師面試題大合集,不看肯定后悔
Redis常見面試題及答案
![<b class='flag-5'>Redis</b><b class='flag-5'>常見面試題</b>及答案](https://file.elecfans.com/web1/M00/D4/49/o4YBAF_Zgw2AICt_AAFCTq2UO_8463.png)
常見的MySQL高頻面試題
面試題:監(jiān)控Redis哪些指標(biāo)
![<b class='flag-5'>面試題</b>:監(jiān)控<b class='flag-5'>Redis</b>哪些指標(biāo)](https://file.elecfans.com/web2/M00/1F/C7/poYBAGGa8vaAdzEGAAAKzRUuZt0399.png)
評(píng)論