引言
作為Redis中最節省內存的數據結構之一,整數集合(intset) 專門用于高效存儲整型數據。但你可能不知道,它背后藏著一個精妙的「動態升級」機制——能在不浪費內存的前提下,靈活適配不同大小的整數。今天我們就來扒開這層神秘面紗,徹底搞懂它的升級策略!
一、先搞清楚:intset到底是啥?
在Redis中,當存儲的整型數據比較「緊湊」(比如都是小整數)時,不會直接用普通的數組或哈希表,而是用更高效的 intset
。它的核心設計目標就一個:用最小的內存存最多的整數。
1.1 intset的底層數組結構
整數集合的底層結構定義(簡化版)如下:
typedef struct intset {uint32_t encoding; // 編碼類型,決定元素的實際類型(如 INTSET_ENC_INT16/INT32/INT64)uint32_t length; // 集合中元素的個數int8_t contents[]; // 存儲元素的數組(實際類型由 encoding 決定)
} intset;
intset
的底層是一個數組(contents[]
),但和普通數組不同,它的元素類型不是固定的,而是由一個「編碼標識」(encoding
)動態決定的。這個 encoding
有三種可能:
INTSET_ENC_INT16
:元素是16位有符號整數(范圍:-32768 ~ 32767),每個元素占2字節;INTSET_ENC_INT32
:元素是32位有符號整數(范圍:-2^31 ~ 2^31-1),每個元素占4字節;INTSET_ENC_INT64
:元素是64位有符號整數(范圍:-2^63 ~ 2^63-1),每個元素占8字節。
舉個栗子:如果一個 intset
的 encoding
是 INTSET_ENC_INT16
,那它的 contents
數組里存的每個數都是16位的,占2字節。
1.2 為什么需要升級?內存優化的核心
假設你有一個 intset
,初始存儲的都是1000以內的小整數(完全在int16范圍內),這時候用int16編碼,每個元素只占2字節,內存利用率極高。但如果突然要插入一個40000的數(超過int16的最大值32767),這時候怎么辦?
直接擴容數組?不行! 因為int16的數組每個位置只能存2字節,40000用int16存會溢出(變成負數)。所以必須升級 encoding
到更大的類型(比如int32),讓所有元素都能被正確存儲。
這就是 intset
升級的核心意義:動態調整編碼類型,用最小的內存兼容所有元素。
二、升級什么時候觸發?惰性策略的智慧
intset
的升級不是「每次插入都檢查」,而是「按需觸發」——只有當你插入一個「當前編碼存不下」的整數時,才會觸發升級。這種「惰性策略」能避免頻繁的內存分配和數據遷移,提升性能。
觸發條件示例:
- 當前
encoding
是INTSET_ENC_INT16
(存16位整數),插入一個32768(超過int16最大值32767)→ 觸發升級到INTSET_ENC_INT32
; - 當前
encoding
是INTSET_ENC_INT32
,插入一個2^32(超過int32最大值2147483647)→ 觸發升級到INTSET_ENC_INT64
; - 插入的數在當前編碼范圍內(比如int16存20000)→ 不升級,直接插入。
三、升級全過程拆解:從int16到int32的「變身」
升級過程聽起來簡單,但背后涉及內存分配、數據類型轉換、元素遷移等步驟。我們以「int16升級到int32」為例,一步步看:
3.1 步驟1:確定目標編碼類型
首先得判斷新插入的數需要多大的空間。比如插入40000,它超過了int16的范圍(-3276832767),但能被int32容納(-2^312^31-1),所以目標編碼是 INTSET_ENC_INT32
。
3.2 步驟2:計算新內存大小
原來的 intset
有3個int16元素(每個2字節),總內存是3×2=6字節。現在要插入1個int32元素(4字節),所有元素都要轉為int32(每個占4字節),所以新內存大小是(3+1)×4=16字節。
3.3 步驟3:分配新內存并遷移數據
這一步是關鍵!Redis會做兩件事:
- 分配新內存:根據計算出的16字節,申請一塊新的連續內存空間;
- 轉換并復制舊數據:把原來的3個int16元素逐個轉為int32(比如10→(int32_t)10,值不變但類型升級),復制到新內存中。
3.4 步驟4:插入新元素并保持有序
intset
中的元素始終是升序排列的(方便二分查找)。所以插入40000時,需要找到正確的位置(這里應該是最后),插入后數組變為 [10, 20, 30, 40000]
。
3.5 步驟5:更新元數據
最后,把 encoding
改為 INTSET_ENC_INT32
,length
加1(現在集合有4個元素)。至此,升級完成!
四、升級的三大關鍵特性:為什么這樣設計?
4.1 類型一致性:升級后「一刀切」
升級完成后,集合里所有元素的類型都統一為目標編碼。比如從int16升級到int32后,后續插入的元素如果超過int32范圍,會再次觸發升級到int64。這保證了數據存儲的一致性,避免了混合類型的復雜處理。
4.2 內存優化:用多少,擴多少
升級只在必要時觸發,避免了「為了存大數而預分配大內存」的浪費。比如存1000個int16整數,只需要2000字節;如果提前升級到int32,就需要4000字節——顯然前者更省內存。
4.3 不可降級:升級容易,降級難
一旦升級到更大的編碼(比如int16→int32),即使后續刪除了所有大整數,encoding
也不會降回去。這是Redis的「性能妥協」:頻繁檢查是否可降級會增加開銷,而保留大編碼對后續插入大數更友好(不需要再次升級)。
五、示例演示:直觀感受升級過程
假設我們有一個初始狀態為 INTSET_ENC_INT16
的 intset
,存儲 [10, 20, 30]
(3個元素,內存6字節):
場景1:插入int16范圍內的數(如25)
- 25在int16范圍內(-32768~32767),無需升級;
- 直接插入到正確位置(升序),集合變為
[10, 20, 25, 30]
,length
=4,內存占用8字節(4×2)。
場景2:插入int32范圍的數(如40000)
- 40000超過int16最大值32767,觸發升級到
INTSET_ENC_INT32
; - 新內存大小=(3+1)×4=16字節;
- 舊數據轉換為int32后復制到新內存,插入40000,集合變為
[10, 20, 30, 40000]
; encoding
改為INTSET_ENC_INT32
,length
=4。
總結:intset升級策略的「得與失」
Redis的 intset
升級策略是一個典型的「空間換時間」與「時間換空間」的平衡設計:
- 優點:通過動態升級,既保證了小整數的內存效率(用int16存小數),又能兼容大整數(升級到int32/int64);
- 缺點:升級需要重新分配內存并復制數據,時間復雜度是O(N)(N是原元素數量),頻繁插入大數可能影響性能。
理解這一策略后,我們在使用Redis時可以更「聰明」:如果確定要存儲大量小整數(如1~1000),可以用 intset
節省內存;如果需要混合存儲大整數,建議直接用 hash
或 string
類型,避免頻繁升級帶來的開銷。