String底層實現——動態字符串SDSredis
面試官:說說Redis的Hash底層 我:......(來自閱文的面試題)bash
跳躍表肯定不瞭解下😏
數據結構
大噶好,今天仍然是元氣滿滿的一天,拋開永遠寫不完的需求,拒絕要求賊變態的客戶,單純的學習技術,感覺技術的魅力。(哈哈哈,皮一下很開森)源碼分析
前面幾周咱們一塊兒看了Redis底層數據結構,如動態字符串SDS
,雙向鏈表Adlist
,字典Dict
,跳躍表
,若是有對Redis常見的類型或底層數據結構不明白的請看上面傳送門。post
今天來講下set的底層實現整數集合
,若是有對set不明白的,常見的API使用這篇就不講了,看上面的傳送門哈。學習
整數集合是Redis設計的一種底層結構,是set的底層實現,當集合中只包含整數值元素,而且這個集合元素數據很少時,會使用這種結構。可是若是不知足剛纔的條件,會使用其餘結構,這邊暫時不講哈。ui
下圖爲整數集合的實際組成,包括三個部分,分別是編碼格式encoding,包含元素數量length,保存元素的數組contents。(這邊只須要簡單看下,下面針對每一個模塊詳細說明哈😝)編碼
咱們看下intset.h裏面關於整數集合的定義,上代碼哈:
//整數集合結構體
typedef struct intset {
uint32_t encoding; //編碼格式,有以下三種格式,初始值默認爲INTSET_ENC_INT16
uint32_t length; //集合元素數量
int8_t contents[]; //保存元素的數組,元素類型並不必定是ini8_t類型,柔性數組不佔intset結構體大小,而且數組中的元素從小到大排列。
} intset;
#define INTSET_ENC_INT16 (sizeof(int16_t)) //16位,2個字節,表示範圍-32,768~32,767
#define INTSET_ENC_INT32 (sizeof(int32_t)) //32位,4個字節,表示範圍-2,147,483,648~2,147,483,647
#define INTSET_ENC_INT64 (sizeof(int64_t)) //64位,8個字節,表示範圍-9,223,372,036,854,775,808~9,223,372,036,854,775,807複製代碼
包括INTSET_ENC_INT16,INTSET_ENC_INT32,INTSET_ENC_INT64三種類型,其分別對應着不一樣的範圍,具體看上面代碼的註釋信息。
由於插入的數據的大小是不同的,爲了儘量的節約內存
(畢竟都是錢,平時要省着點用😭),因此咱們須要使用不一樣的類型來存儲數據。
記錄了保存數據contents的長度,即有多少個元素。
真正存儲數據的地方,數組是按照從小到大
有序排序的,而且不包含任何重複項
(由於set是不含重複項,因此其底層實現也是不含包含項的)。
上面的圖咱們從新看下,編碼格式encoding爲INTSET_ENC_INT16,即每一個數據佔16位。長度length爲4,即數組content裏面有四個元素,分別是1,2,3,4。若是咱們要添加一個數字位40000,很明顯超過編碼格式爲INTSET_ENC_INT16的範圍-32,768~32,767,應該是編碼格式爲INTSET_ENC_INT32。那麼他是如何升級的呢,從INTSET_ENC_INT16升級到INTSET_ENC_INT32的呢?
首先咱們看下1,2,3,4這四個元素是如何存儲的。首先要知道一共有多少位,計算規則爲length*編碼格式的位數
,即4*16=64
。因此每一個元素佔用了16位。
新的元素爲40000,已經超過了INTSET_ENC_INT16的範圍-32,768~32,767,因此新的編碼格式爲INTSET_ENC_INT32。
上面已經說明了編碼格式爲INTSET_ENC_INT32,計算規則爲length*編碼格式的位數
,即5*32=160
。因此新增的位數爲64-159。
從上面知道按照新的編碼格式,每一個數據應該佔用32位,可是舊的編碼格式,每一個數據佔用16位。因此咱們從後面開始,每次獲取32位用來存儲數據。
這樣說太難懂了,看下圖☺。
首先,那最後32位,即128-159存儲40000。那麼第49-127是空着的。
接着,取空着的49-127最後的32位,即96到127這32位,用來存儲4。那麼以前4存儲的位置48-63
和49-127剩下的64-95
這兩部分組成了一個大部分,即48-95
,如今空着啦。
在接着在48-95這個大部分,再取後32位,即64-95,用來存儲3。那麼以前3存儲位置32-47
和48-95剩下的48-63
這兩部分組成了一個大部分,即32-63
,如今空着啦。
再接着,將32-63這個大部分,再取後32位,即仍是32-63,用來存儲2。那麼以前2存儲位置16-31空着啦。
最後,將16-31和原來0-31合起來,存儲1。
至此,整個升級過程結束。總體來講,分爲3步,肯定新的編碼格式,新增須要的內存空間,從後往前調整數據。
這邊有個小問題,爲啥要從後往前調整數據呢?
緣由是若是從前日後,數據可能會覆蓋。也拿上面個例子來講,數據1在0-15位,數據2在16-31位,若是從前日後,咱們知道新的編碼格式INTSET_ENC_INT32要求每一個元素佔用32位,那麼數據1應該佔用0-31,這個時候數據2就被覆蓋了,之後就不知道數據2啦。
可是從後往前,由於後面新增了一些內存,因此不會發生覆蓋現象。
整數集合既可讓集合保存三種不一樣類型的值,又能夠確保升級操做只在有須要的時候進行,這樣就節省了內存。
一旦對數組進行升級,編碼就會一直保存升級後的狀態。即便後面把40000刪掉了,編碼格式仍是不會將會INTSET_ENC_INT16。
這個方法比較簡單,是初始化整數集合的步驟,即下圖部分。
主要的步驟是分配內存空間,設置默認編碼格式,以及初始化數組長度length。
intset *intsetNew(void) {
intset *is = zmalloc(sizeof(intset));//分配內存空間
is->encoding = intrev32ifbe(INTSET_ENC_INT16);//設置默認編碼格式INTSET_ENC_INT16
is->length = 0;//初始化length
return is;
}
複製代碼
能夠根據上面的流程圖,對照着下面的源碼分析,這邊就不寫啦哈。
//添加元素
//輸入參數*is爲原整數集合
//value爲要添加的元素
//*success爲是否添加成功的標誌量 ,1表示成功,0表示失敗
intset *intsetAdd(intset *is, int64_t value, uint8_t *success) {
//肯定要添加的元素的編碼格式
uint8_t valenc = _intsetValueEncoding(value);
uint32_t pos;
//若是success沒有初始值,則初始化爲1
if (success) *success = 1;
//若是新的編碼格式大於如今的編碼格式,則升級並添加元素
if (valenc > intrev32ifbe(is->encoding)) {
//調用另外一個方法
return intsetUpgradeAndAdd(is,value);
} else {
//若是編碼格式不變,則調用查詢方法
//輸入參數is爲原整數集合
//value爲要添加的數據
//pos爲位置
if (intsetSearch(is,value,&pos)) {//若是找到了,則直接返回,由於數據是不可重複的。
if (success) *success = 0;
return is;
}
//設置length
is = intsetResize(is,intrev32ifbe(is->length)+1);
if (pos < intrev32ifbe(is->length)) intsetMoveTail(is,pos,pos+1);
}
//設置數據
_intsetSet(is,pos,value);
is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
return is;
}
//#define INT8_MAX 127
//#define INT16_MAX 32767
//#define INT32_MAX 2147483647
//#define INT64_MAX 9223372036854775807LL
static uint8_t _intsetValueEncoding(int64_t v) {
if (v < INT32_MIN || v > INT32_MAX)
return INTSET_ENC_INT64;
else if (v < INT16_MIN || v > INT16_MAX)
return INTSET_ENC_INT32;
else
return INTSET_ENC_INT16;
}
//根據輸入參數value的編碼格式,對整數集合is的編碼格式升級
static intset *intsetUpgradeAndAdd(intset *is, int64_t value) {
//當前集合的編碼格式
uint8_t curenc = intrev32ifbe(is->encoding);
//根據對value解析獲取新的編碼格式
uint8_t newenc = _intsetValueEncoding(value);
//獲取集合元素數量
int length = intrev32ifbe(is->length);
//若是要添加的數據小於0,則prepend爲1,不然爲0
int prepend = value < 0 ? 1 : 0;
//設置集合爲新的編碼格式,並根據編碼格式從新設置內存
is->encoding = intrev32ifbe(newenc);
is = intsetResize(is,intrev32ifbe(is->length)+1);
//逐步循環,直到length小於0,挨個從新設置每一個值,從後往前
while(length--)
_intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc));
//若是value爲負數,則放在最前面
if (prepend)
_intsetSet(is,0,value);
else//若是value爲整數,設置最末尾的元素爲value
_intsetSet(is,intrev32ifbe(is->length),value);
//從新設置length
is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
return is;
}
//找到is集合中值爲value的下標,返回1,並保存在pos中,沒有找到返回0,並將pos設置爲value能夠插入到數組的位置
static uint8_t intsetSearch(intset *is, int64_t value, uint32_t *pos) {
int min = 0, max = intrev32ifbe(is->length)-1, mid = -1;
int64_t cur = -1;
//若是集合爲空,那麼位置pos爲0
if (intrev32ifbe(is->length) == 0) {
if (pos) *pos = 0;
return 0;
} else {
//由於數據是有序集合,若是要添加的數據大於最後一個數字,那麼直接把要添加的值放在最後便可,返回最大值下標
if (value > _intsetGet(is,intrev32ifbe(is->length)-1)) {
if (pos) *pos = intrev32ifbe(is->length);
return 0;
} else if (value < _intsetGet(is,0)) { //若是這個數據小於數組下標爲0的數據,即爲最小值 ,返回0
if (pos) *pos = 0;
return 0;
}
}
//有序集合採用二分法
while(max >= min) {
mid = ((unsigned int)min + (unsigned int)max) >> 1;
cur = _intsetGet(is,mid);
if (value > cur) {
min = mid+1;
} else if (value < cur) {
max = mid-1;
} else {
break;
}
}
//肯定找到
if (value == cur) {
if (pos) *pos = mid;//設置參數pos,返回1,即找到位置
return 1;
} else {//若是沒找到,則min和max相鄰,隨便設置都行,並返回0
if (pos) *pos = min;
return 0;
}
}複製代碼
該篇主要講了Redis的SET數據類型的底層實現整數集合,先從整數集合是什麼,,剖析了其主要組成部分,進而經過多幅過程圖解釋了intset是如何升級的,最後結合源碼對整數集合進行描述,如建立過程,升級過程,中間穿插例子和過程圖。
若是以爲寫得還行,麻煩給個贊👍,您的承認纔是我寫做的動力!
若是以爲有說的不對的地方,歡迎評論指出。
好了,拜拜咯。
1.若是本文對你有幫助,就點個贊支持下吧,你的「贊」是我創做的動力。
2.關注公衆號「學習Java的小姐姐」便可加我好友,我拉你進「Java技術交流羣」,你們一塊兒共同交流和進步。