目錄
一、先懂定位:為什么需要整數集合?(銜接哈希表)
二、整數集合的結構:像 “貼了規格標簽的收納盒”
1. encoding:收納盒的 “規格標簽”(核心:決定格子大小)
2. length:收納盒的 “物品數量”
3. contents:收納盒的 “有序格子”
三、核心操作:“升級”—— 從小盒子換大盒子(重點)
舉個具體例子:從 int16 升級到 int32
步驟 1:換大收納盒(擴展空間)
步驟 2:把小珠子換成大珠子(轉換元素類型)
步驟 3:放進新的大珠子(添加新元素)
3.1、為什么只能升級,不能降級?(Redis 的權衡)
四、適用場景:什么時候用整數集合?(對比哈希表)
五、總結:整數集合的設計邏輯(銜接 Redis 整體哲學)
如果說 SDS 是 “智能價簽”、壓縮列表是 “緊湊貨架”、字典是 “萬能儲物柜”,那整數集合(intset)?就是 Redis 為 “少量整數” 定制的分類收納盒—— 專門用來存純整數、數量不多的集合(比如SADD nums 10 20 30
),核心優勢是 “省空間、保有序、無重復”。它和哈希表(hashtable)是集合鍵的兩種底層實現,就像收納時 “少量小物件用分類盒,大量 / 雜物件用儲物柜”。
一、先懂定位:為什么需要整數集合?(銜接哈希表)
之前學的哈希表(hashtable)?雖然能存集合(比如非整數元素SADD fruits "apple"
),但有個 “內存浪費” 的問題:每個元素要存鍵(元素本身)和值(NULL,因為集合只需存存在性),還要處理哈希沖突。比如存 3 個小整數10、20、30
,哈希表的指針、哈希槽等開銷,比整數本身的空間還大。
而整數集合的思路是:用 “統一規格的數組” 存整數,不存額外指針 / 哈希信息,還保持有序無重復—— 就像用 “帶格子的收納盒” 裝小珠子,每個格子只放一顆,按大小排好,沒有多余的包裝,省空間又好查找。
二、整數集合的結構:像 “貼了規格標簽的收納盒”
整數集合的底層是intset
結構體,對應 “分類收納盒” 的三個核心部分:規格標簽、物品數量、收納格子。我們先看結構定義,再逐個類比拆解:
typedef struct intset {uint32_t encoding; // 收納盒的“規格標簽”(存什么類型的整數)uint32_t length; // 收納盒里的“物品數量”(元素個數)int8_t contents[]; // 收納盒的“格子數組”(實際存整數的地方)
} intset;
1. encoding:收納盒的 “規格標簽”(核心:決定格子大小)
encoding
就像收納盒上貼的 “適用物品規格”,比如 “僅放直徑≤5mm 的小珠子”,它決定了contents
數組實際能存什么類型的整數:
- INTSET_ENC_INT16:“小格子規格”—— 每個格子存
int16_t
類型(范圍:-32768 ~ 32767),比如 10、20、32767。 - INTSET_ENC_INT32:“中格子規格”—— 每個格子存
int32_t
類型(范圍:-2147483648 ~ 2147483647),比如 32768(超過 int16 最大值)、100000。 - INTSET_ENC_INT64:“大格子規格”—— 每個格子存
int64_t
類型(范圍極大),比如 2147483648(超過 int32 最大值)。
關鍵注意:雖然
contents
聲明是int8_t
(1 字節小格子),但實際格子大小由encoding
決定!就像收納盒表面寫 “小格子”,但貼的規格標簽是 “中格子”,實際就能放大珠子 —— 這是 Redis 的 “靈活聲明”,為了兼容不同規格。
2. length:收納盒的 “物品數量”
length
直接記錄收納盒里有多少個整數(無重復),比如存了 10、20、30,length=3
。和壓縮列表的zllen
類似,能快速獲取元素數量(O (1) 復雜度),不用數格子。
3. contents:收納盒的 “有序格子”
contents
是實際存整數的數組,有兩個核心特性:
- 有序:按從小到大排列,比如存 10、30、20,最終格子里是
[10,20,30]
—— 方便快速查找(用二分法,O (logN) 復雜度)。 - 無重復:同一個整數不會出現兩次,比如再存 20,Redis 會檢查格子里已有,不重復添加 —— 就像收納盒里不會有兩顆一樣大的珠子。
三、核心操作:“升級”—— 從小盒子換大盒子(重點)
整數集合最特殊的邏輯是 “自動升級”:當要放的整數 “太大”(超過當前encoding
的范圍),就必須把整個收納盒換成更大規格,再統一轉換原有元素的 “大小”,最后放進新元素。這就像 “小珠子收納盒里要放大珠子,必須先換大盒子,把原來的小珠子都換成大珠子,再放大珠子”。
舉個具體例子:從 int16 升級到 int32
假設現在有個 “小格子收納盒”(encoding=INTSET_ENC_INT16),已經放了兩個小整數:10、32767(int16 的最大值)。現在要放 32768(超過 int16 的最大值,必須用 int32),升級步驟對應生活場景:
步驟 1:換大收納盒(擴展空間)
- 原來的小盒子:每個格子占 2 字節(int16),2 個元素共 4 字節。
- 新的大盒子:每個格子占 4 字節(int32),需要存 3 個元素(原有 2 個 + 新 1 個),總空間 = 3×4=12 字節。
- Redis 會申請 12 字節的新空間,代替原來的 4 字節 —— 就像把 2 格的小盒子,換成 3 格的大盒子。
步驟 2:把小珠子換成大珠子(轉換元素類型)
- 不能直接把小珠子(int16 的 10、32767)放進大格子,必須先 “放大” 成大珠子(int32 的 10、32767)。
- 轉換時要保持順序:原來小盒子里是
[10,32767]
,轉換后大盒子的前兩格還是[10,32767]
(只是每個占 4 字節)—— 就像把小珠子裝進大托里,位置不變。
步驟 3:放進新的大珠子(添加新元素)
- 把 32768(int32 類型)放進大盒子的最后一格,最終格子里是
[10,32767,32768]
,保持有序。 - 最后更新
encoding
為 INTSET_ENC_INT32,length
為 3—— 收納盒的規格標簽和數量標簽同步更新。
3.1、為什么只能升級,不能降級?(Redis 的權衡)
升級后就算刪了 “大珠子”(比如把 32768 刪掉,只剩 10、32767),Redis 也不會把大盒子換回小盒子 —— 這就是 “無降級” 規則,原因很簡單:
- 麻煩且沒必要:降級需要先檢查所有元素是否都符合小規格,再轉換類型、縮小空間 —— 就像把大珠子換回小珠子,再換小盒子,步驟多,浪費時間。
- 效率優先:Redis 認為 “少量內存浪費” 比 “頻繁降級的性能損耗” 更劃算。比如大盒子里放小珠子,多占的內存不多(每個元素多 2 字節),但省去了降級的麻煩。
- 避免風險:如果降級后又要放大珠子,得再次升級,反復操作反而更慢 —— 就像換了小盒子又要換大的,不如一直用大的。
四、適用場景:什么時候用整數集合?(對比哈希表)
Redis 選擇整數集合還是哈希表,遵循 “整數優先、少量優先” 的原則,和壓縮列表的 “小數據優先” 思路一致:
集合類型 | 底層實現 | 原因(生活類比) |
---|---|---|
純整數元素 + 元素少(默認<512 個) | 整數集合 | 分類收納盒存少量小珠子,省空間、好排序 |
非整數元素(如字符串) | 哈希表 | 萬能儲物柜存雜物件,不管類型,查找快 |
純整數元素 + 元素多 | 哈希表 | 收納盒格子太多不好翻,儲物柜(哈希表)更快 |
舉例:
SADD nums 1 2 3
(純整數、少元素)→ 整數集合;
SADD fruits "apple" "banana"
(非整數)→ 哈希表;
SADD big_nums 1 2 ... 1000
(多元素)→ 哈希表。
五、總結:整數集合的設計邏輯(銜接 Redis 整體哲學)
整數集合的所有設計,都和之前學的 SDS、壓縮列表、跳躍表一脈相承,體現 Redis“因地制宜、權衡取舍” 的核心思路:
- SDS:用 “len+free” 平衡 “字符串擴展” 和 “內存浪費”—— 取舍:預分配少量空間換擴展效率;
- 壓縮列表:用 “連續內存 + 可變標簽” 平衡 “內存節省” 和 “連鎖更新風險”—— 取舍:接受罕見更新換極致省內存;
- 整數集合:用 “統一規格數組 + 自動升級” 平衡 “內存節省” 和 “類型兼容”—— 取舍:不支持降級換操作簡單、效率高。
簡單說,整數集合就是 Redis 為 “少量整數集合” 量身定制的 “高效收納盒”:它沒有哈希表的靈活,但勝在省空間、有序;沒有壓縮列表的復雜,勝在操作簡單、只存整數。這種 “專物專用” 的設計,正是 Redis 能兼顧性能和內存的關鍵。