Redis系統架構中各個處理模塊是干什么的?
Redis 系統架構
本課時,我將進一步分析 Redis 的系統架構,重點講解 Redis 系統架構的事件處理機制、數據管理、功能擴展、系統擴展等內容。
事件處理機制
Redis 組件的系統架構如圖所示,主要包括事件處理、數據存儲及管理、用于系統擴展的主從復制/集群管理,以及為插件化功能擴展的 Module System 模塊。
Redis 中的事件處理模塊,采用的是作者自己開發的 ae 事件驅動模型
,可以進行高效的網絡 IO 讀寫、命令執行,以及時間事件處理。
其中,網絡 IO 讀寫處理采用的是 IO 多路復用技術,通過對 evport、epoll、kqueue、select 等進行封裝,同時監聽多個 socket,并根據 socket 目前執行的任務,來為 socket 關聯不同的事件處理器。
當監聽端口對應的 socket 收到連接請求后,就會創建一個 client 結構,通過 client 結構來對連接狀態進行管理。在請求進入時,將請求命令讀取緩沖并進行解析,并存入到 client 的參數列表。
然后根據請求命令找到 對應的redisCommand ,最后根據命令協議,對請求參數進一步的解析、校驗并執行。Redis 中時間事件比較簡單,目前主要是執行 serverCron,來做一些統計更新、過期 key 清理、AOF 及 RDB 持久化等輔助操作。
數據管理
Redis 的內存數據都存在 redisDB 中。Redis 支持多 DB,每個 DB 都對應一個 redisDB 結構。Redis 的 8 種數據類型,每種數據類型都采用一種或多種內部數據結構進行存儲。同時這些內部數據結構及數據相關的輔助信息,都以 kye/value 的格式存在 redisDB 中的各個 dict 字典中。
數據在寫入 redisDB 后,這些執行的寫指令還會及時追加到 AOF 中,追加的方式是先實時寫入AOF 緩沖,然后按策略刷緩沖數據到文件。由于 AOF 記錄每個寫操作,所以一個 key 的大量中間狀態也會呈現在 AOF 中,導致 AOF 冗余信息過多,因此 Redis 還設計了一個 RDB 快照操作,可以通過定期將內存里所有的數據快照落地到 RDB 文件,來以最簡潔的方式記錄 Redis 的所有內存數據。
Redis 進行數據讀寫的核心處理線程是單線程模型,為了保持整個系統的高性能,必須避免任何kennel 導致阻塞的操作。為此,Redis 增加了 BIO 線程,來處理容易導致阻塞的文件 close、fsync 等操作,確保系統處理的性能和穩定性。
在 server 端,存儲內存永遠是昂貴且短缺的,Redis 中,過期的 key 需要及時清理,不活躍的 key 在內存不足時也可能需要進行淘汰。為此,Redis 設計了 8 種淘汰策略,借助新引入的 eviction pool,進行高效的 key 淘汰和內存回收。
功能擴展
Redis 在 4.0 版本之后引入了 Module System 模塊,可以方便使用者,在不修改核心功能的同時,進行插件化功能開發。使用者可以將新的 feature 封裝成動態鏈接庫,Redis 可以在啟動時加載,也可以在運行過程中隨時按需加載和啟用。
在擴展模塊中,開發者可以通過 RedisModule_init 初始化新模塊,用 RedisModule_CreateCommand 擴展各種新模塊指令,以可插拔的方式為 Redis 引入新的數據結構和訪問命令。
系統擴展
Redis作者在架構設計中對系統的擴展也傾注了大量關注。在主從復制功能中,psyn 在不斷的優化,不僅在 slave 閃斷重連后可以進行增量復制,而且在 slave 通過主從切換成為 master 后,其他 slave 仍然可以與新晉升的 master 進行增量復制,另外,其他一些場景,如 slave 重啟后,也可以進行增量復制,大大提升了主從復制的可用性。使用者可以更方便的使用主從復制,進行業務數據的讀寫分離,大幅提升 Redis 系統的穩定讀寫能力。
通過主從復制可以較好的解決 Redis 的單機讀寫問題,但所有寫操作都集中在 master 服務器,很容易達到 Redis 的寫上限,同時 Redis 的主從節點都保存了業務的所有數據,隨著業務發展,很容易出現內存不夠用的問題。
為此,Redis 分區無法避免。雖然業界大多采用在 client 和 proxy 端分區,但 Redis 自己也早早推出了 cluster 功能,并不斷進行優化。Redis cluster 預先設定了 16384 個 slot 槽,在 Redis 集群啟動時,通過手動或自動將這些 slot 分配到不同服務節點上。在進行 key 讀寫定位時,首先對 key 做 hash,并將 hash 值對 16383 ,做 按位與運算,確認 slot,然后確認服務節點,最后再對 對應的 Redis 節點,進行常規讀寫。如果 client 發送到錯誤的 Redis 分片,Redis 會發送重定向回復。如果業務數據大量增加,Redis 集群可以通過數據遷移,來進行在線擴容。
Redis如何處理文件事件和時間事件?
Redis時間驅動模型
Redis 是一個事件驅動程序,但和 Memcached 不同的是,Redis 并沒有采用 libevent 或 libev 這些開源庫,而是直接開發了一個新的事件循環組件。Redis 作者給出的理由是,盡量減少外部依賴,而自己開發的事件模型也足夠簡潔、輕便、高效,也更易控制。Redis 的事件驅動模型機制封裝在 aeEventLoop 等相關的結構體中,網絡連接、命令讀取執行回復,數據的持久化、淘汰回收 key 等,幾乎所有的核心操作都通過 ae 事件模型進行處理。
Redis 的事件驅動模型處理 2 類事件:
- 文件事件,如連接建立、接受請求命令、發送響應等;
- 時間事件,如 Redis 中定期要執行的統計、key 淘汰、緩沖數據寫出、rehash等。
文件事件處理
Redis 的文件事件采用典型的 Reactor 模式進行處理。Redis 文件事件處理機制分為 4 部分:
- 連接socket
- IO多路復用程序
- 文件事件分派器
- 事件處理器
文件事件是對連接 socket 操作的一個抽象。當端口監聽 socket 準備 accept 新連接,或者連接 socket 準備好讀取請求、寫入響應、關閉時,就會產生一個文件事件。IO 多路復用程序負責同時監聽多個 socket,當這些 socket 產生文件事件時,就會觸發事件通知,文件分派器就會感知并獲取到這些事件。
雖然多個文件事件可能會并發出現,但 IO 多路復用程序總會將所有產生事件的 socket 放入一個隊列中,通過這個隊列,有序的把這些文件事件通知給文件分派器。
IO多路復用
Redis 封裝了 4 種多路復用程序,每種封裝實現都提供了相同的 API 實現。編譯時,會按照性能和系統平臺,選擇最佳的 IO 多路復用函數作為底層實現,選擇順序是,首先嘗試選擇 Solaries 中的 evport,如果沒有,就嘗試選擇 Linux 中的 epoll,否則就選擇大多 UNIX 系統都支持的 kqueue,這 3 個多路復用函數都直接使用系統內核內部的結構,可以服務數十萬的文件描述符。
如果當前編譯環境沒有上述函數,就會選擇 select 作為底層實現方案。select 方案的性能較差,事件發生時,會掃描全部監聽的描述符,事件復雜度是 O(n),并且只能同時服務有限個文件描述符,32 位機默認是 1024 個,64 位機默認是 2048 個,所以一般情況下,并不會選擇 select 作為線上運行方案。Redis 的這 4 種實現,分別在 ae_evport、ae_epoll、ae_kqueue 和 ae_select 這 4 個代碼文件中。
文件事件收集及派發器
Redis 中的文件事件分派器是 aeProcessEvents 函數。它會首先計算最大可以等待的時間,然后利用 aeApiPoll 等待文件事件的發生。如果在等待時間內,一旦 IO 多路復用程序產生了事件通知,則會立即輪詢所有已產生的文件事件,并將文件事件放入 aeEventLoop 中的 aeFiredEvents 結構數組中。每個 fired event 會記錄 socket 及 Redis 讀寫事件類型。
這里會涉及將多路復用中的事件類型,轉換為 Redis 的 ae 事件驅動模型中的事件類型。以采用 Linux 中的 epoll 為例,會將 epoll 中的 EPOLLIN 轉為 AE_READABLE 類型,將 epoll 中的 EPOLLOUT、EPOLLERR 和 EPOLLHUP 轉為 AE_WRITABLE 事件。
aeProcessEvents 在獲取到觸發的事件后,會根據事件類型,將文件事件 dispatch 派發給對應事件處理函數。如果同一個 socket,同時有讀事件和寫事件,Redis 派發器會首先派發處理讀事件,然后再派發處理寫事件。
文件事件處理函數分類
Redis 中文件事件函數的注冊和處理主要分為 3 種。
- 連接處理函數 acceptTcpHandler
Redis 在啟動時,在 initServer 中對監聽的 socket 注冊讀事件,事件處理器為 acceptTcpHandler,該函數在有新連接進入時,會被派發器派發讀任務。在處理該讀任務時,會 accept 新連接,獲取調用方的 IP 及端口,并對新連接創建一個 client 結構。如果同時有大量連接同時進入,Redis 一次最多處理 1000 個連接請求。
- readQueryFromClient 請求處理函數
連接函數在創建 client 時,會對新連接 socket 注冊一個讀事件,該讀事件的事件處理器就是 readQueryFromClient。在連接 socket 有請求命令到達時,IO 多路復用程序會獲取并觸發文件事件,然后這個讀事件被派發器派發給本請求的處理函數。readQueryFromClient 會從連接 socket 讀取數據,存入 client 的 query 緩沖,然后進行解析命令,按照 Redis 當前支持的 2 種請求格式,及 inline 內聯格式和 multibulk 字符塊數組格式進行嘗試解析。解析完畢后,client 會根據請求命令從命令表中獲取到對應的 redisCommand,如果對應 cmd 存在。則開始校驗請求的參數,以及當前 server 的內存、磁盤及其他狀態,完成校驗后,然后真正開始執行 redisCommand 的處理函數,進行具體命令的執行,最后將執行結果作為響應寫入 client 的寫緩沖中。
- 命令回復處理器 sendReplyToClient
當 redis需要發送響應給client時,Redis 事件循環中會對client的連接socket注冊寫事件,這個寫事件的處理函數就是sendReplyToClient。通過注冊寫事件,將 client 的socket與 AE_WRITABLE 進行間接關聯。當 Client fd 可進行寫操作時,就會觸發寫事件,該函數就會將寫緩沖中的數據發送給調用方。
Redis 中的時間事件是指需要在特定時間執行的事件。多個 Redis 中的時間事件構成 aeEventLoop 中的一個鏈表,供 Redis 在 ae 事件循環中輪詢執行。
Redis 當前的主要時間事件處理函數有 2 個:
- serverCron
- moduleTimerHandler
Redis 中的時間事件分為 2 類:
- 單次時間,即執行完畢后,該時間事件就結束了。
- 周期性事件,在事件執行完畢后,會繼續設置下一次執行的事件,從而在時間到達后繼續執行,并不斷重復。
時間事件主要有 5 個屬性組成。
- 事件 ID:Redis 為時間事件創建全局唯一 ID,該 ID 按從小到大的順序進行遞增。
- 執行時間 when_sec 和 when_ms:精確到毫秒,記錄該事件的到達可執行時間。
- 時間事件處理器 timeProc:在時間事件到達時,Redis 會調用相應的 timeProc 處理事件。
- 關聯數據 clientData:在調用 timeProc 時,需要使用該關聯數據作為參數。
- 鏈表指針 prev 和 next:它用來將時間事件維護為雙向鏈表,便于插入及查找所要執行的時間事件。
時間事件的處理是在事件循環中的 aeProcessEvents 中進行。執行過程是:
- 首先遍歷所有的時間事件。
- 比較事件的時間和當前時間,找出可執行的時間事件。
- 然后執行時間事件的 timeProc 函數。
- 執行完畢后,對于周期性時間,設置時間新的執行時間;對于單次性時間,設置事件的 ID為 -1,后續在事件循環中,下一次執行 aeProcessEvents 的時候從鏈表中刪除。
Redis讀取請求數據后,如何進行協議解析和處理?
協議解析
上一課時講到,請求命令進入,觸發 IO 讀事件后。client 會從連接文件描述符讀取請求,并存入 client 的 query buffer 中。client 的讀緩沖默認是 16KB,讀取命令時,如果發現請求超過 1GB,則直接報異常,關閉連接。
client 讀取完請求命令后,則根據 query buff 進行協議解析。協議解析時,首先查看協議的首字符。如果是 *,則解析為字符塊數組類型,即 MULTIBULK。否則請求解析為 INLINE 類型。
INLINE 類型是以 CRLF 結尾的單行字符串,協議命令及參數以空格分隔。解析過程參考之前課程里分析的對應協議格式。協議解析完畢后,將請求參數個數存入 client 的 argc 中,將請求的具體參數存入 client 的 argv 中。
協議執行
請求命令解析完畢,則進入到協議執行部分。協議執行中,對于 quit 指令,直接返回 OK,設置 flag 為回復后關閉連接。
對于非 quit 指令,以 client 中 argv[0] 作為命令,從 server 中的命令表中找到對應的 redisCommand。如果沒有找到 redisCommand,則返回未知 cmd 異常。如果找到 cmd,則開始執行 redisCommand 中的 proc 函數,進行具體命令的執行。在命令執行完畢后,將響應寫入 client 的寫緩沖。并按配置和部署,將寫指令分發給 aof 和 slaves。同時更新相關的統計數值。
怎么認識和應用Redis內部數據結構?
RedisDb
Redis 中所有數據都保存在 DB 中,一個 Redis 默認最多支持 16 個 DB。Redis 中的每個 DB 都對應一個 redisDb 結構,即每個 Redis 實例,默認有 16 個 redisDb。用戶訪問時,默認使用的是 0 號 DB,可以通過 select $dbID 在不同 DB 之間切換。
redisDb 主要包括 2 個核心 dict 字典、3 個非核心 dict 字典、dbID 和其他輔助屬性。2 個核心 dict 包括一個 dict 主字典和一個 expires 過期字典。主 dict 字典用來存儲當前 DB 中的所有數據,它將 key 和各種數據類型的 value 關聯起來,該 dict 也稱 key space。過期字典用來存儲過期時間 key,存的是 key 與過期時間的映射。日常的數據存儲和訪問基本都會訪問到 redisDb 中的這兩個 dict。
3 個非核心 dict 包括一個字段名叫 blocking_keys 的阻塞 dict,一個字段名叫 ready_keys 的解除阻塞 dict,還有一個是字段名叫 watched_keys 的 watch 監控 dict。
在執行 Redis 中 list 的阻塞命令 blpop、brpop 或者 brpoplpush 時,如果對應的 list 列表為空,Redis 就會將對應的 client 設為阻塞狀態,同時將該 client 添加到 DB 中 blocking_keys 這個阻塞 dict。所以該 dict 存儲的是處于阻塞狀態的 key 及 client 列表。
當有其他調用方在向某個 key 對應的 list 中增加元素時,Redis 會檢測是否有 client 阻塞在這個 key 上,即檢查 blocking_keys 中是否包含這個 key,如果有則會將這個 key 加入 read_keys 這個 dict 中。同時也會將這個 key 保存到 server 中的一個名叫 read_keys 的列表中。這樣可以高效、不重復的插入及輪詢。
當 client 使用 watch 指令來監控 key 時,這個 key 和 client 就會被保存到 watched_keys 這個 dict 中。redisDb 中可以保存所有的數據類型,而 Redis 中所有數據類型都是存放在一個叫 redisObject 的結構中。
redisObject
redisObject 由 5 個字段組成。
- type:即 Redis 對象的數據類型,目前支持 7 種 type 類型,分別為
- OBJ_STRING
- OBJ_LIST
- OBJ_SET
- OBJ_ZSET
- OBJ_HASH
- OBJ_MODULE
- OBJ_STREAM
- encoding:Redis 對象的內部編碼方式,即內部數據結構類型,目前支持 10 種編碼方式包括
- OBJ_ENCODING_RAW
- OBJ_ENCODING_INT
- OBJ_ENCODING_HT
- OBJ_ENCODING_ZIPLIST 等。
- LRU:存儲的是淘汰數據用的 LRU 時間或 LFU 頻率及時間的數據。
- refcount:記錄 Redis 對象的引用計數,用來表示對象被共享的次數,共享使用時加 1,不再使用時減 1,當計數為 0 時表明該對象沒有被使用,就會被釋放,回收內存。
- ptr:它指向對象的內部數據結構。比如一個代表 string 的對象,它的 ptr 可能指向一個 sds 或者一個 long 型整數。
dict
前面講到,Redis 中的數據實際是存在 DB 中的 2 個核心 dict 字典中的。實際上 dict 也是 Redis 的一種使用廣泛的內部數據結構。
Redis 中的 dict,類似于 Memcached 中 hashtable。都可以用于 key 或元素的快速插入、更新和定位。dict 字典中,有一個長度為 2 的哈希表數組,日常訪問用 0 號哈希表,如果 0 號哈希表元素過多,則分配一個 2 倍 0 號哈希表大小的空間給 1 號哈希表,然后進行逐步遷移,rehashidx 這個字段就是專門用來做標志遷移位置的。在哈希表操作中,采用單向鏈表來解決 hash 沖突問題。dict 中還有一個重要字段是 type,它用于保存 hash 函數及 key/value 賦值、比較函數。
dictht 中的 table 是一個 hash 表數組,每個桶指向一個 dictEntry 結構。dictht 采用 dictEntry 的單向鏈表來解決 hash 沖突問題。
dictht 是以 dictEntry 來存 key-value 映射的。其中 key 是 sds 字符串,value 為存儲各種數據類型的 redisObject 結構。
dict 可以被 redisDb 用來存儲數據 key-value 及命令操作的輔助信息。還可以用來作為一些 Redis 數據類型的內部數據結構。dict 可以作為 set 集合的內部數據結構。在哈希的元素數超過 512 個,或者哈希中 value 大于 64 字節,dict 還被用作為哈希類型的內部數據結構。
sds
字符串是 Redis 中最常見的數據類型,其底層實現是簡單動態字符串即 sds。簡單動態字符串本質是一個 char*,內部通過 sdshdr 進行管理。sdshdr 有 4 個字段。len 為字符串實際長度,alloc 當前字節數組總共分配的內存大小。flags 記錄當前字節數組的屬性;buf 是存儲字符串真正的值及末尾一個 \0。
sds 的存儲 buf 可以動態擴展或收縮,字符串長度不用遍歷,可直接獲得,修改和訪問都很方便。由于 sds 中字符串存在 buf 數組中,長度由 len 定義,而不像傳統字符串遇 0 停止,所以 sds 是二進制安全的,可以存放任何二進制的數據。
簡單動態字符串 sds 的獲取字符串長度很方便,通過 len 可以直接得到,而傳統字符串需要對字符串進行遍歷,時間復雜度為 O(n)。
sds 相比傳統字符串多了一個 sdshdr,對于大量很短的字符串,這個 sdshdr 還是一個不小的開銷。在 3.2 版本后,sds 會根據字符串實際的長度,選擇不同的數據結構,以更好的提升內存效率。當前 sdshdr 結構分為 5 種子類型,分別為 sdshdr5、sdshdr8、sdshdr16、sdshdr32、sdshdr64。其中 sdshdr5 只有 flags 和 buf 字段,其他幾種類型的 len 和 alloc 采用從 uint8_t 到 uint64_t 的不同類型,以節省內存空間。
sds 可以作為字符串的內部數據結構,同時 sds 也是 hyperloglog、bitmap 類型的內部數據結構。
ziplist
為了節約內存,并減少內存碎片,Redis 設計了 ziplist 壓縮列表內部數據結構。壓縮列表是一塊連續的內存空間,可以連續存儲多個元素,沒有冗余空間,是一種連續內存數據塊組成的順序型內存結構。
ziplist 的結構如圖所示,主要包括 5 個部分。
- zlbytes 是壓縮列表所占用的總內存字節數。
- Zltail 尾節點到起始位置的字節數。
- Zllen 總共包含的節點/內存塊數。
- Entry 是 ziplist 保存的各個數據節點,這些數據點長度隨意。
- Zlend 是一個魔數 255,用來標記壓縮列表的結束。
如圖所示,一個包含 4 個元素的 ziplist,總占用字節是 100bytes,該 ziplist 的起始元素的指針是 p,zltail 是 80,則第 4 個元素的指針是 P+80。
壓縮列表 ziplist 的存儲節點 entry 的結構如圖,主要有 6 個字段。
- prevRawLen 是前置節點的長度;
- preRawLenSize 編碼 preRawLen 需要的字節數;
- len 當前節點的長度;
- lensize 編碼 len 所需要的字節數;
- encoding 當前節點所用的編碼類型;
- entryData 當前節點數據。
由于 ziplist 是連續緊湊存儲,沒有冗余空間,所以插入新的元素需要 realloc 擴展內存,所以如果 ziplist 占用空間太大,realloc 重新分配內存和拷貝的開銷就會很大,所以 ziplist 不適合存儲過多元素,也不適合存儲過大的字符串。
因此只有在元素數和 value 數都不大的時候,ziplist 才作為 hash 和 zset 的內部數據結構。其中 hash 使用 ziplist 作為內部數據結構的限制時,元素數默認不超過 512 個,value 值默認不超過 64 字節。可以通過修改配置來調整 hash_max_ziplist_entries 、hash_max_ziplist_value 這兩個閥值的大小。
zset 有序集合,使用 ziplist 作為內部數據結構的限制元素數默認不超過 128 個,value 值默認不超過 64 字節。可以通過修改配置來調整 zset_max_ziplist_entries 和 zset_max_ziplist_value 這兩個閥值的大小。
quicklist
Redis 在 3.2 版本之后引入 quicklist,用以替換 linkedlist。因為 linkedlist 每個節點有前后指針,要占用 16 字節,而且每個節點獨立分配內存,很容易加劇內存的碎片化。而 ziplist 由于緊湊型存儲,增加元素需要 realloc,刪除元素需要內存拷貝,天然不適合元素太多、value 太大的存儲。
而 quicklist 快速列表應運而生,它是一個基于 ziplist 的雙向鏈表。將數據分段存儲到 ziplist,然后將這些 ziplist 用雙向指針連接。快速列表的結構如圖所示。
- head、tail 是兩個指向第一個和最后一個 ziplist 節點的指針。
- count 是 quicklist 中所有的元素個數。
- len 是 ziplist 節點的個數。
- compress 是 LZF 算法的壓縮深度。
快速列表中,管理 ziplist 的是 quicklistNode 結構。quicklistNode 主要包含一個 prev/next 雙向指針,以及一個 ziplist 節點。單個 ziplist 節點可以存放多個元素。
快速列表從頭尾讀寫數據很快,時間復雜度為 O(1)。也支持從中間任意位置插入或讀寫元素,但速度較慢,時間復雜度為 O(n)。快速列表當前主要作為 list 列表的內部數據結構。
zskiplist
跳躍表 zskiplist 是一種有序數據結構,它通過在每個節點維持多個指向其他節點的指針,從而可以加速訪問。跳躍表支持平均 O(logN) 和最差 O(n) 復雜度的節點查找。在大部分場景,跳躍表的效率和平衡樹接近,但跳躍表的實現比平衡樹要簡單,所以不少程序都用跳躍表來替換平衡樹。
如果 sorted set 類型的元素數比較多或者元素比較大,Redis 就會選擇跳躍表來作為 sorted set有序集合的內部數據結構。
跳躍表主要由 zskipList 和節點 zskiplistNode 構成。zskiplist 結構如圖,header 指向跳躍表的表頭節點。tail 指向跳躍表的表尾節點。length 表示跳躍表的長度,它是跳躍表中不包含表頭節點的節點數量。level 是目前跳躍表內,除表頭節點外的所有節點中,層數最大的那個節點的層數。
跳躍表的節點 zskiplistNode 的結構如圖所示。ele 是節點對應的 sds 值,在 zset 有序集合中就是集合中的 field 元素。score 是節點的分數,通過 score,跳躍表中的節點自小到大依次排列。backward 是指向當前節點的前一個節點的指針。level 是節點中的層,每個節點一般有多個層。每個 level 層都帶有兩個屬性,一個是 forwad 前進指針,它用于指向表尾方向的節點;另外一個是 span 跨度,它是指 forward 指向的節點到當前節點的距離。
如圖所示是一個跳躍表,它有 3 個節點。對應的元素值分別是 S1、S2 和 S3,分數值依次為 1.0、3.0 和 5.0。其中 S3 節點的 level 最大是 5,跳躍表的 level 是 5。header 指向表頭節點,tail 指向表尾節點。在查到元素時,累加路徑上的跨度即得到元素位置。在跳躍表中,元素必須是唯一的,但 score 可以相同。相同 score 的不同元素,按照字典序進行排序。
在 sorted set 數據類型中,如果元素數較多或元素長度較大,則使用跳躍表作為內部數據結構。默認元素數超過 128 或者最大元素的長度超過 64,此時有序集合就采用 zskiplist 進行存儲。由于 geo 也采用有序集合類型來存儲地理位置名稱和位置 hash 值,所以在超過相同閥值后,也采用跳躍表進行存儲。
Redis 主要的內部數據結構講完了,接下來整體看一下,之前講的 8 種數據類型,具體都是采用哪種內部數據結構來存儲的。
首先,對于 string 字符串,Redis 主要采用 sds 來進行存儲。而對于 list 列表,Redis 采用 quicklist 進行存儲。對于 set 集合類型,Redis 采用 dict 來進行存儲。對于 sorted set 有序集合類型,如果元素數小于 128 且元素長度小于 64,則使用 ziplist 存儲,否則使用 zskiplist 存儲。對于哈希類型,如果元素數小于 512,并且元素長度小于 64,則用 ziplist 存儲,否則使用 dict 字典存儲。對于 hyperloglog,采用 sds 簡單動態字符串存儲。對于 geo,如果位置數小于 128,則使用 ziplist 存儲,否則使用 zskiplist 存儲。最后對于 bitmap,采用 sds 簡單動態字符串存儲。