Redis數據結構之整數集合

一、整數集合

Redis 中有集合(set)的操做,經常使用的指令有 SADD、SCARD 等,而在底層的實現中,整數集合(intset)就是 Redis 集合的實現方式之一。數據庫

Redis 的集合是有序集合,intset 也是有序的。api

根據 Redis 對集合的操做,咱們能夠大體想象出,intset 須要哪些功能:數組

  1. 添加/移除元素要快;
  2. 查找元素要快;
  3. 方便兩個集合對比;
  4. 統計元素個數;
  5. 佔用內存要少;

Redis 做爲高性能數據庫,佔用內存和操做快速是最主要的功能。intset 爲了保證除了在保證快速的同時,使用升級來保證可以存儲數據可是又節省空間的需求。數據結構

Redis 的 intset 相對來講比較簡單,下面也就大體的描述一下dom

二、intset的實現

intset 聲明位於源碼目錄下 intset.h 文件中,聲明以下:性能

typedef struct intset {
    uint32_t encoding;
    uint32_t length;
    int8_t contents[];
} intset;

contents: contents 是一個柔性數組,這裏很少說,它裏面存儲的就是 intset 的全部整數項;ui

須要注意的是,雖然 contents 是 int8_t,不表示 contents 內部只能存 int8_t 類型的數據,後面會細說;code

length: length 記錄了整個集合的元素數量,即 contents 數組的長度,當須要獲取元素個數的時候,直接返回這個值就好了,時間複雜度 O(1);索引

encoding: encoding 決定該集合存儲的整數的類型,它的值有三種,以下:內存

/* Note that these encodings are ordered, so:
 * INTSET_ENC_INT16 < INTSET_ENC_INT32 < INTSET_ENC_INT64. */
#define INTSET_ENC_INT16 (sizeof(int16_t))
#define INTSET_ENC_INT32 (sizeof(int32_t))
#define INTSET_ENC_INT64 (sizeof(int64_t))

能夠看到,encoding 其實就能夠表示 int8_t、int16_t、int32_t 和 int64_t(32位編譯器下分別表示佔用1一個、2個、4個 和 8個字節,換作大部分人能認識的就是 char、short、int 和 long),其實能夠看出來,encoding 就是這幾種類型的佔用內存,因此,若是如要計算集合中的全部元素所佔的內存,只須要用 length * encoding 就能夠了

Redis 中使用 encoding 無非是爲了節約內存,假如全部元素都是 int8_t,開闢了 int16_t 的空間,白白的就是在浪費內存嘛。

2.一、intset數據的存儲

那麼,contents 數組內是如何兼容不一樣類型的整數呢?咱們都知道,C語言中最小的數據結構 char 佔8位,而1六、3二、64位均是8位的整數倍,因此咱們只須要知道當前數組存儲的數據類型,就能夠根據本身位計算出數據來,而不關注這個數組的長度了。

好比,如今 contents 數組共佔16位,從低到高分別是:

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
1 1 1 0 0 1 0 1 1 0 0 0 1 1 0 1

若是 encoding 表示8位元素,那麼,這裏能夠得出兩個 int8_t 數據出來,分別是 229 和 141;

若是 encoding 表示16位元素,那麼,這裏能夠得出一個 int16_t 數據出來,是 58765;

可見,不是說 contents 數組的類型是 int8_t 就只能存8位的數據,可是,contents 只能存儲一種類型的數據。

三、升級和降級

當咱們須要將一個新的元素放入到集合中,而且這個新的數據比集合中的其餘元素的類型的最大值還要大的時候,Redis 就須要統一使用較大的類型來存儲了,即須要擴容,這在 Redis 中叫作升級(upgrade)。

Redis 中升級集合並添加新元素總共須要三步:

  1. 根據新元素的大小,肯定數組的類型,併爲數組分配空間;
  2. 將底層已存在的舊有轉換成新的類型,並按照原先的順序,放置在固定的內存位置上;
  3. 將新元素放在數組裏。

由於集合中的元素都是有序的,就算咱們不須要進行升級,仍然須要從頭遍歷元素,也就是仍是上面三步,因此插入元素的時間複雜度爲 O(N)。

一旦出現須要升級操做,則表示新元素必定比舊有的元素要大,因此新元素放在最後就好。

舉個例子,如今有一個集合,其中有三個 int8_t 類型的元素:一、二、3,則 intset 中 encoding 值小於 INTSET_ENC_INT16,length 爲3,contents 數組所佔內存爲 8 * 3 = 24bit,即3個字節,數據佔內存位置以下:

0~7位 8~15位 16~23位
元素 1 2 3

如今插入一個 int16_t 類型的元素 32,咱們發現,encoding 的值小於 INTSET_ENC_INT16,因此須要擴容,content 從新分配內存,16 *4 = 64bit,即8個字節。擴容後的內存以下:

0~7位 8~15位 16~23位 24~63位
元素 1 2 3 新分配的空間

隨後,將原先的一、二、3轉換成 int16_t 類型,並按照原先的順序放置在固定位置,以下:

0~15位 16~31位 32~47位 48~63位
元素 1 2 3 新分配

最後,將新元素放在數字的最後,即

0~15位 16~31位 32~47位 48~63位
元素 1 2 3 32

元素放置完畢,encoding 值變爲 INTSET_ENC_INT16, length 變爲 4

Redis 的 intset 是不支持降級操做的,一旦數據升級,就會保持升級以後的類型,哪怕惟一個佔用內存大的元素被刪除了,剩下的元素仍然佔用大的類型的元素佔用的內存。

四、inset操做API

intset.h 文件中聲明瞭 intset 操做的API,整理以下:

API 描述 時間複雜度
intsetNew 建立一個新的inset O(1)
intsetAdd 添加新元素 O(N)
intsetRemove 移除元素 O(N)
intsetFind 查找元素 有序集合可使用二分法,O(logN)
intsetRandom 隨機返回一個元素 O(1)
intsetGet 根據索引取出元素 O(1)
intsetLen 獲取元素個數 O(1)
intsetBlobLen 獲取佔用內存字節數 O(1)
相關文章
相關標籤/搜索