...

硬核!15張圖解Redis爲(wéi / wèi)什麽這(zhè)麽快

2021-07-13

 

 作者|萊烏

 

作爲(wéi / wèi)一(yī / yì /yí)名服務端工程師,工作中你肯定和(hé / huò) Redis 打過交道(dào)。Redis 爲(wéi / wèi)什麽快,這(zhè)點想必你也(yě)知道(dào),至少爲(wéi / wèi)了(le/liǎo)面試也(yě)做過準備。很多人(rén)知道(dào) Redis 快僅僅因爲(wéi / wèi)它是(shì)基于(yú)内存實現的(de),對于(yú)其它原因倒是(shì)模棱兩可。

 

那麽今天就(jiù)和(hé / huò)小萊一(yī / yì /yí)起看看:

 

 

 - 思維導圖 -

 



基于(yú)内存實現

 

這(zhè)點在(zài)一(yī / yì /yí)開始就(jiù)提到(dào)過了(le/liǎo),這(zhè)裏再簡單說(shuō)說(shuō)。

 

Redis 是(shì)基于(yú)内存的(de)數據庫,那不(bù)可避免的(de)就(jiù)要(yào / yāo)與磁盤數據庫做對比。對于(yú)磁盤數據庫來(lái)說(shuō),是(shì)需要(yào / yāo)将數據讀取到(dào)内存裏的(de),這(zhè)個(gè)過程會受到(dào)磁盤 I/O 的(de)限制。

 

而(ér)對于(yú)内存數據庫來(lái)說(shuō),本身數據就(jiù)存在(zài)于(yú)内存裏,也(yě)就(jiù)沒有了(le/liǎo)這(zhè)方面的(de)開銷。

 



高效的(de)數據結構

 

Redis 中有多種數據類型,每種數據類型的(de)底層都由一(yī / yì /yí)種或多種數據結構來(lái)支持。正是(shì)因爲(wéi / wèi)有了(le/liǎo)這(zhè)些數據結構,Redis 在(zài)存儲與讀取上(shàng)的(de)速度才不(bù)受阻礙。這(zhè)些數據結構有什麽特别的(de)地(dì / de)方,各位看官接着往下看:

 

1、簡單動态字符串

這(zhè)個(gè)名詞可能你不(bù)熟悉,換成 SDS 肯定就(jiù)知道(dào)了(le/liǎo)。這(zhè)是(shì)用來(lái)處理字符串的(de)。了(le/liǎo)解 C 語言的(de)都知道(dào),它是(shì)有處理字符串方法的(de)。而(ér) Redis 就(jiù)是(shì) C 語言實現的(de),那爲(wéi / wèi)什麽還要(yào / yāo)重複造輪子(zǐ)?我們從以(yǐ)下幾點來(lái)看:

 

(1)字符串長度處理

 

 

 這(zhè)個(gè)圖是(shì)字符串在(zài) C 語言中的(de)存儲方式,想要(yào / yāo)獲取 「Redis」的(de)長度,需要(yào / yāo)從頭開始遍曆,直到(dào)遇到(dào) '\0' 爲(wéi / wèi)止。

 

 

  Redis 中怎麽操作呢?用一(yī / yì /yí)個(gè) len 字段記錄當前字符串的(de)長度。想要(yào / yāo)獲取長度隻需要(yào / yāo)獲取 len 字段即可。你看,差距不(bù)言自明。前者遍曆的(de)時(shí)間複雜度爲(wéi / wèi) O(n),Redis 中 O(1) 就(jiù)能拿到(dào),速度明顯提升。

 

(2)内存重新分配

 

C 語言中涉及到(dào)修改字符串的(de)時(shí)候會重新分配内存。修改地(dì / de)越頻繁,内存分配也(yě)就(jiù)越頻繁。而(ér)内存分配是(shì)會消耗性能的(de),那麽性能下降在(zài)所難免。

 

而(ér) Redis 中會涉及到(dào)字符串頻繁的(de)修改操作,這(zhè)種内存分配方式顯然就(jiù)不(bù)适合了(le/liǎo)。于(yú)是(shì) SDS 實現了(le/liǎo)兩種優化策略:

 

  • 空間預分配

 

對 SDS 修改及空間擴充時(shí),除了(le/liǎo)分配所必須的(de)空間外,還會額外分配未使用的(de)空間。

 

具體分配規則是(shì)這(zhè)樣的(de):SDS 修改後,len 長度小于(yú) 1M,那麽将會額外分配與 len 相同長度的(de)未使用空間。如果修改後長度大(dà)于(yú) 1M,那麽将分配1M的(de)使用空間。

 

  • 惰性空間釋放

當然,有空間分配對應的(de)就(jiù)有空間釋放。

 

SDS 縮短時(shí),并不(bù)會回收多餘的(de)内存空間,而(ér)是(shì)使用 free 字段将多出(chū)來(lái)的(de)空間記錄下來(lái)。如果後續有變更操作,直接使用 free 中記錄的(de)空間,減少了(le/liǎo)内存的(de)分配。

 

(3)二進制安全

你已經知道(dào)了(le/liǎo) Redis 可以(yǐ)存儲各種數據類型,那麽二進制數據肯定也(yě)不(bù)例外。但二進制數據并不(bù)是(shì)規則的(de)字符串格式,可能會包含一(yī / yì /yí)些特殊的(de)字符,比如 '\0' 等。

 

前面我們提到(dào)過,C 中字符串遇到(dào) '\0' 會結束,那 '\0' 之(zhī)後的(de)數據就(jiù)讀取不(bù)上(shàng)了(le/liǎo)。但在(zài) SDS 中,是(shì)根據 len 長度來(lái)判斷字符串結束的(de)。

 

看,二進制安全的(de)問題就(jiù)解決了(le/liǎo)。

 

2、雙端鏈表

 

列表 List 更多是(shì)被當作隊列或棧來(lái)使用的(de)。隊列和(hé / huò)棧的(de)特性一(yī / yì /yí)個(gè)先進先出(chū),一(yī / yì /yí)個(gè)先進後出(chū)。雙端鏈表很好的(de)支持了(le/liǎo)這(zhè)些特性。

 

 

 - 雙端鏈表 -

 

(1)前後節點

 

 鏈表裏每個(gè)節點都帶有兩個(gè)指針,prev 指向前節點,next 指向後節點。這(zhè)樣在(zài)時(shí)間複雜度爲(wéi / wèi) O(1) 内就(jiù)能獲取到(dào)前後節點。

 

(2)頭尾節點

 

 你可能注意到(dào)了(le/liǎo),頭節點裏有 head 和(hé / huò) tail 兩個(gè)參數,分别指向頭節點和(hé / huò)尾節點。這(zhè)樣的(de)設計能夠對雙端節點的(de)處理時(shí)間複雜度降至 O(1) ,對于(yú)隊列和(hé / huò)棧來(lái)說(shuō)再适合不(bù)過。同時(shí)鏈表叠代時(shí)從兩端都可以(yǐ)進行。

 

(3)鏈表長度

頭節點裏同時(shí)還有一(yī / yì /yí)個(gè)參數 len,和(hé / huò)上(shàng)邊提到(dào)的(de) SDS 裏類似,這(zhè)裏是(shì)用來(lái)記錄鏈表長度的(de)。因此獲取鏈表長度時(shí)不(bù)用再遍曆整個(gè)鏈表,直接拿到(dào) len 值就(jiù)可以(yǐ)了(le/liǎo),這(zhè)個(gè)時(shí)間複雜度是(shì) O(1)。

 

你看,這(zhè)些特性都降低了(le/liǎo) List 使用時(shí)的(de)時(shí)間開銷。

 

3、壓縮列表

雙端鏈表我們已經熟悉了(le/liǎo)。不(bù)知道(dào)你有沒有注意到(dào)一(yī / yì /yí)個(gè)問題:如果在(zài)一(yī / yì /yí)個(gè)鏈表節點中存儲一(yī / yì /yí)個(gè)小數據,比如一(yī / yì /yí)個(gè)字節。那麽對應的(de)就(jiù)要(yào / yāo)保存頭節點,前後指針等額外的(de)數據。

 

這(zhè)樣就(jiù)浪費了(le/liǎo)空間,同時(shí)由于(yú)反複申請與釋放也(yě)容易導緻内存碎片化。這(zhè)樣内存的(de)使用效率就(jiù)太低了(le/liǎo)。

 

于(yú)是(shì),壓縮列表上(shàng)場了(le/liǎo)!

 

它是(shì)經過特殊編碼,專門爲(wéi / wèi)了(le/liǎo)提升内存使用效率設計的(de)。所有的(de)操作都是(shì)通過指針與解碼出(chū)來(lái)的(de)偏移量進行的(de)。

 

并且壓縮列表的(de)内存是(shì)連續分配的(de),遍曆的(de)速度很快。

 

4、字典

Redis 作爲(wéi / wèi) K-V 型數據庫,所有的(de)鍵值都是(shì)用字典來(lái)存儲的(de)。

 

日常學習中使用的(de)字典你應該不(bù)會陌生,想查找某個(gè)詞通過某個(gè)字就(jiù)可以(yǐ)直接定位到(dào),速度非常快。這(zhè)裏所說(shuō)的(de)字典原理上(shàng)是(shì)一(yī / yì /yí)樣的(de),通過某個(gè) key 可以(yǐ)直接獲取到(dào)對應的(de)value。

 

字典又稱爲(wéi / wèi)哈希表,這(zhè)點沒什麽可說(shuō)的(de)。哈希表的(de)特性大(dà)家都很清楚,能夠在(zài) O(1) 時(shí)間複雜度内取出(chū)和(hé / huò)插入關聯的(de)值。

 

5、跳躍表

 

作爲(wéi / wèi) Redis 中特有的(de)數據結構-跳躍表,其在(zài)鏈表的(de)基礎上(shàng)增加了(le/liǎo)多級索引來(lái)提升查找效率。

 

 

 這(zhè)是(shì)跳躍表的(de)簡單原理圖,每一(yī / yì /yí)層都有一(yī / yì /yí)條有序的(de)鏈表,最底層的(de)鏈表包含了(le/liǎo)所有的(de)元素。這(zhè)樣跳躍表就(jiù)可以(yǐ)支持在(zài) O(logN) 的(de)時(shí)間複雜度裏查找到(dào)對應的(de)節點。

 

下面這(zhè)張是(shì)跳表真實的(de)存儲結構,和(hé / huò)其它數據結構一(yī / yì /yí)樣,都在(zài)頭節點裏記錄了(le/liǎo)相應的(de)信息,減少了(le/liǎo)一(yī / yì /yí)些不(bù)必要(yào / yāo)的(de)系統開銷。

 

 

 合理的(de)數據編碼

 

對于(yú)每一(yī / yì /yí)種數據類型來(lái)說(shuō),底層的(de)支持可能是(shì)多種數據結構,什麽時(shí)候使用哪種數據結構,這(zhè)就(jiù)涉及到(dào)了(le/liǎo)編碼轉化的(de)問題。

 

那我們就(jiù)來(lái)看看,不(bù)同的(de)數據類型是(shì)如何進行編碼轉化的(de):

 

String:存儲數字的(de)話,采用int類型的(de)編碼,如果是(shì)非數字的(de)話,采用 raw 編碼;

 

List:字符串長度及元素個(gè)數小于(yú)一(yī / yì /yí)定範圍使用 ziplist 編碼,任意條件不(bù)滿足,則轉化爲(wéi / wèi) linkedlist 編碼;

 

Hash:hash 對象保存的(de)鍵值對内的(de)鍵和(hé / huò)值字符串長度小于(yú)一(yī / yì /yí)定值及鍵值對;

 

Set:保存元素爲(wéi / wèi)整數及元素個(gè)數小于(yú)一(yī / yì /yí)定範圍使用 intset 編碼,任意條件不(bù)滿足,則使用 hashtable 編碼;

 

Zset:zset 對象中保存的(de)元素個(gè)數小于(yú)及成員長度小于(yú)一(yī / yì /yí)定值使用 ziplist 編碼,任意條件不(bù)滿足,則使用 skiplist 編碼。

 



合适的(de)線程模型

 

Redis 快的(de)原因還有一(yī / yì /yí)個(gè)是(shì)因爲(wéi / wèi)使用了(le/liǎo)合适的(de)線程模型:

 

1、I/O多路複用模型

  • I/O :網絡 I/O

  • 多路:多個(gè) TCP 連接

  • 複用:共用一(yī / yì /yí)個(gè)線程或進程

 

生産環境中的(de)使用,通常是(shì)多個(gè)客戶端連接 Redis,然後各自發送命令至 Redis 服務器,最後服務端處理這(zhè)些請求返回結果。

 

 

 應對大(dà)量的(de)請求,Redis 中使用 I/O 多路複用程序同時(shí)監聽多個(gè)套接字,并将這(zhè)些事件推送到(dào)一(yī / yì /yí)個(gè)隊列裏,然後逐個(gè)被執行。最終将結果返回給客戶端。

 

2、避免上(shàng)下文切換

你一(yī / yì /yí)定聽說(shuō)過,Redis 是(shì)單線程的(de)。那麽單線程的(de) Redis 爲(wéi / wèi)什麽會快呢?

 

因爲(wéi / wèi)多線程在(zài)執行過程中需要(yào / yāo)進行 CPU 的(de)上(shàng)下文切換,這(zhè)個(gè)操作比較耗時(shí)。Redis 又是(shì)基于(yú)内存實現的(de),對于(yú)内存來(lái)說(shuō),沒有上(shàng)下文切換效率就(jiù)是(shì)最高的(de)。多次讀寫都在(zài)一(yī / yì /yí)個(gè)CPU 上(shàng),對于(yú)内存來(lái)說(shuō)就(jiù)是(shì)最佳方案。

 

3、單線程模型

順便提一(yī / yì /yí)下,爲(wéi / wèi)什麽 Redis 是(shì)單線程的(de)。

 

Redis 中使用了(le/liǎo) Reactor 單線程模型,你可能對它并不(bù)熟悉。沒關系,隻需要(yào / yāo)大(dà)概了(le/liǎo)解一(yī / yì /yí)下即可。

 

這(zhè)張圖裏,接收到(dào)用戶的(de)請求後,全部推送到(dào)一(yī / yì /yí)個(gè)隊列裏,然後交給文件事件分派器,而(ér)它是(shì)單線程的(de)工作方式。Redis 又是(shì)基于(yú)它工作的(de),所以(yǐ)說(shuō) Redis 是(shì)單線程的(de)。

 
總結

 

基于(yú)内存實現

  • 數據都存儲在(zài)内存裏,減少了(le/liǎo)一(yī / yì /yí)些不(bù)必要(yào / yāo)的(de) I/O 操作,操作速率很快。

高效的(de)數據結構

  • 底層多種數據結構支持不(bù)同的(de)數據類型,支持 Redis 存儲不(bù)同的(de)數據;

  • 不(bù)同數據結構的(de)設計,使得數據存儲時(shí)間複雜度降到(dào)最低。

合理的(de)數據編碼

  • 根據字符串的(de)長度及元素的(de)個(gè)數适配不(bù)同的(de)編碼格式。

 

合适的(de)線程模型

  • I/O 多路複用模型同時(shí)監聽客戶端連接;

  • 單線程在(zài)執行過程中不(bù)需要(yào / yāo)進行上(shàng)下文切換,減少了(le/liǎo)耗時(shí)。


來(lái)源:cnblogs