文章目錄
- 為什么 Redis 不直接使用 C 語言的字符串?
- sdshdr 的結構
- sdshdr 的不同類型
- sdshdr 帶來的優勢總結
我們來詳細解析一下 Redis 的核心數據結構之一:
sdshdr
。
sdshdr
是 “Simple Dynamic String header” 的縮寫,意為“簡單動態字符串頭”。它是在 Redis 自己實現的字符串庫(SDS)中,用于定義字符串對象的頭部結構。理解了 sdshdr
,就能明白為什么 Redis 的字符串操作如此高效和安全。
簡單來說,sdshdr
是 Redis 字符串(SDS)的元數據部分,它緊鄰實際的字符串數據存放在同一塊連續的內存中,記錄了字符串的長度、空余空間等信息。
為什么 Redis 不直接使用 C 語言的字符串?
要理解 sdshdr
的重要性,首先要明白傳統 C 語言字符串(以 \0
結尾的字符數組)的缺陷:
- 獲取長度效率低: C 語言字符串本身不記錄長度,要獲取其長度必須遍歷整個字符串,直到遇到
\0
,時間復雜度為 O(N)O(N)O(N)。 - 容易造成緩沖區溢出(Buffer Overflow): 當使用
strcat
等函數拼接字符串時,如果目標數組空間不足,就會發生緩沖區溢出,這是一種嚴重的安全漏洞。 - 二進制不安全: C 語言字符串以
\0
(空字符) 作為結束符,這意味著字符串內容不能包含\0
。因此,無法用它來存儲圖片、音頻等二進制數據。 - 內存管理復雜: 每次增長或縮短字符串,都需要手動進行復雜的內存重分配,容易出錯且效率不高。
為了解決這些問題,Redis 設計了 SDS。而 SDS 的核心就是 sdshdr
頭部結構。
sdshdr 的結構
一個完整的 SDS 字符串在內存中由兩部分組成:
- 頭部(Header): 即
sdshdr
結構體。 - 數據(Data): 緊跟在頭部后面的實際字符串內容。
sdshdr
并不是一個單一的結構,為了節省內存,Redis 根據字符串的實際長度,定義了多種不同的 sdshdr
類型(在 sds.h
源碼中定義)。
在 Redis 5.0 及以后的版本中,sdshdr
的通用結構可以看作是:
struct __attribute__ ((__packed__)) sdshdr<T> {T len; // 已使用長度 (length of the string)T alloc; // 總分配長度 (total allocated length, excluding header and null terminator)unsigned char flags; // 標志位 (flags, indicating the header type)char buf[]; // 柔性數組 (flexible array member), 代表實際的字符串數據
};
關鍵字段解釋:
len
: 記錄了buf
中已存儲字符串的實際長度。有了它,Redis 獲取字符串長度的時間復雜度是 O(1)O(1)O(1),極其高效。alloc
: 記錄了不包括頭部和末尾\0
的情況下,總共為buf
分配的內存空間大小。len
和alloc
的差值就是剩余可用空間。flags
: 一個3位的字段,用來表示當前sdshdr
的具體類型。buf[]
: 這是一個“柔性數組成員”,是 C99 的一個特性。它表示buf
指向sdshdr
結構體之后緊跟的內存地址,這里存放著實際的字符串內容。字符串的末尾同樣會追加一個\0
,以兼容部分 C 語言函數庫。
__attribute__ ((__packed__))
是一個 GCC 的指令,用于告訴編譯器取消結構體在編譯過程中的內存對齊優化,使得結構體成員緊湊排列,從而節省內存。
sdshdr 的不同類型
根據 flags
字段的值,Redis 會使用不同的 sdshdr
結構,主要區別在于 len
和 alloc
字段的數據類型,從而節省頭部占用的空間:
flags 值 | 類型 | len 和 alloc 的數據類型 | 頭部大小 |
---|---|---|---|
0 | sdshdr5 | (沒有 len /alloc 字段) | 1 字節 |
1 | sdshdr8 | uint8_t (8位無符號整數) | 3 字節 |
2 | sdshdr16 | uint16_t (16位無符號整數) | 5 字節 |
3 | sdshdr32 | uint32_t (32位無符號整數) | 9 字節 |
4 | sdshdr64 | uint64_t (64位無符號整數) | 17 字節 |
特別說明 sdshdr5
:
sdshdr5
是一個特例,它沒有 len
和 alloc
字段。它的 flags
字段本身就編碼了字符串的長度(高5位存長度,低3位存類型)。它只能用于存儲非常短的字符串。
Redis 會根據字符串的長度自動選擇最小的、能容納該字符串的 sdshdr
類型,實現極致的內存優化。
sdshdr 帶來的優勢總結
基于 sdshdr
結構,Redis 的 SDS 相比 C 語言字符串獲得了巨大優勢:
- 常數時間復雜度的長度獲取: 直接讀取
len
屬性即可,時間復雜度為 O(1)O(1)O(1)。 - 杜絕緩沖區溢出: 當對 SDS 進行修改時(如
APPEND
),SDS 的 API 會先檢查alloc - len
的剩余空間是否足夠。如果不足,它會自動進行內存重分配,擴展buf
的大小,然后再執行操作,從而保證了安全。(對這里len和alloc不理解的可以看文章末尾!!!) - 空間預分配與惰性釋放(減少內存重分配次數):
- 空間預分配: 當對 SDS 進行擴展時,如果修改后字符串長度小于 1MB,程序會分配
len * 2
的空間;如果超過 1MB,則會額外多分配 1MB 的空間。這種策略避免了每次增加字符串都重新分配內存,提升了性能。 - 惰性空間釋放: 當縮短 SDS 字符串時,程序并不會立即釋放多出來的空間,而是更新
len
字段,將這部分空間記錄為未使用,以備將來再次使用。
- 空間預分配: 當對 SDS 進行擴展時,如果修改后字符串長度小于 1MB,程序會分配
- 二進制安全: SDS 使用
len
屬性來判斷字符串結束,而不是\0
。因此buf
中可以包含任意字符,包括\0
。這使得 SDS 可以安全地存儲任何二進制數據。 - 兼容部分 C 語言函數: SDS 字符串的末尾依然保留了一個
\0
字符(這個\0
不計入len
長度),這使得那些只讀取而不修改字符串的 C 語言函數(如printf
、strcmp
)可以直接處理 SDS 的buf
部分。
綜上所述,sdshdr
是 Redis 高性能字符串實現的關鍵基石。它通過一個精巧的頭部設計,解決了傳統 C 語言字符串的諸多痛點,為 Redis 提供了高效、安全且功能豐富的字符串處理能力。
下一篇:
Redis中的sdshdr的len和alloc那塊的知識點詳解