1. 程式人生 > >Redis 的底層資料結構(整數集合)

Redis 的底層資料結構(整數集合)

當一個集合中只包含整數,並且元素的個數不是很多的話,redis 會用整數集合作為底層儲存,它的一個優點就是可以節省很多記憶體,雖然字典結構的效率很高,但是它的實現結構相對複雜並且會分配較多的記憶體空間。

而我們的整數集合(intset)可以做到使用較少的記憶體空間卻達到和字典一樣效率的實現,但也是前提的,集合中只能包含整型資料並且數量不能太多。整數集合最多能存多少個元素在 redis 中也是有體現的。

OBJ_SET_MAX_INTSET_ENTRIES 512

也就是超過 512 個元素,或者向集合中添加了字串或其他資料結構,redis 會將整數集合向字典結構進行轉換。

一、基本的資料結構

intset 的結構定義很簡單,有以下成員構成:

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

encoding 記錄當前 intset 使用編碼,有三個取值:

#define INTSET_ENC_INT16 (sizeof(int16_t))
#define INTSET_ENC_INT32 (sizeof(int32_t))
#define INTSET_ENC_INT64 (sizeof(int64_t))

length 記錄整數集合中目前儲存了多少個元素,contents 記錄我們實際的資料集合,雖然我們看到結構體中給陣列元素的型別定死成 int8_t,但實際上這個 int8_t 定義的毫無意義,因為這裡的處理方式非常規的陣列操作,content 欄位雖然被定義成指向一個 int8_t 型別資料的指標,但實際上 redis 無論是讀取陣列元素還是新增元素進去都依賴 encoding 和 length 兩個欄位直接操作的記憶體。

基本資料結構還是非常的簡單的,下面我們來看看它的一些核心方法。

二、核心 API 實現

1、初始化一個 intset

intset *intsetNew(void) {
    intset *is = zmalloc(sizeof(intset));
    is->encoding = intrev32ifbe(INTSET_ENC_INT16);
    is->length = 0;
    return is;
}

可見,預設的 inset 配置是使用 INTSET_ENC_INT16 作為資料儲存大小,並且不會為 content 陣列初始化。常規的陣列需要先預先確定陣列長度,然後分配記憶體,繼而通過 contents[x] 可以訪問陣列中任一元素。

但是,inset 這裡是非常規式運算元組,encoding 欄位定義了陣列中每個元素實際型別,lenth 欄位定義了陣列中實際的元素個數,那麼 contents[x] 是失效的,這種方式只會按照 int8_t 進行記憶體偏移,這種方式是拿不到正確的資料的,所以 redis 中通過 memcpy 按照 encoding 欄位的值暴力直接偏移地址操作記憶體讀取資料。

所以,這也是為什麼 intset 初始化時不初始化 content 陣列的原因所在,因為沒有必要。而每當新增一個元素的時候都會去動態擴容原陣列的長度以盛放下新插入進來的元素,擴容不會擴容很多,剛好一個新元素所佔用的記憶體即可。具體的細節,我們接著看。

2、新增新元素

intset *intsetAdd(intset *is, int64_t value, uint8_t *success) {
    //計算得到新插入的元素的編碼
    uint8_t valenc = _intsetValueEncoding(value);
    uint32_t pos;
    if (success) *success = 1;
    //如果大於 intset 目前儲存元素的編碼大小
    if (valenc > intrev32ifbe(is->encoding)) {
        //觸發 intset 升級
        return intsetUpgradeAndAdd(is,value);
    } else {
        //二分搜尋當前元素,如果元素已經存在會直接返回
        //如果沒找到元素,pos 的值就是該元素的位置索引
        if (intsetSearch(is,value,&pos)) {
            if (success) *success = 0;
            return is;
        }
        //resize 集合,擴容一個元素的記憶體空間
        is = intsetResize(is,intrev32ifbe(is->length)+1);
        //移動 pos 後面的元素,以插入我們的新元素
        if (pos < intrev32ifbe(is->length)) intsetMoveTail(is,pos,pos+1);
    }
    //賦值
    _intsetSet(is,pos,value);
    is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
    return is;
}

由此,我們應該知道為什麼 intset 內的資料是有序且無重複的了,二分查詢 O(logN),但是 intset 插入一個元素卻不是 O(logN),因為有些情況會觸發升級操作,或者極端情況下,會移動所有元素,時間複雜度達到 O(N)。

3、升級

我們先看示意圖的變化,然後再分析原始碼,假設原 intset 使用 16 位的編碼儲存資料,先來了一個 32 位的資料,觸發了我們的編碼升級。

原 intset 結構如下:

新 intset 結構會擴容成這樣:

雖然資料佔用的記憶體已經分配好了,但是還需要做的是遷移每個元素佔用的位元位。
做法是這樣的,假設我們的新元素是 int_32 型別的數值 65536,那麼首先我們會將這個 65536 放到[128-159]位元位區間,然後將 78 放到[96-127]位元位區間,並向前以此類推,最後我們會得到升級完成之後 intset。

下面我們看 redis 中程式碼的實現:

static intset *intsetUpgradeAndAdd(intset *is, int64_t value) {
    //intset目前的編碼
    uint8_t curenc = intrev32ifbe(is->encoding);
    //intset即將擴充套件到的編碼
    uint8_t newenc = _intsetValueEncoding(value);
    int length = intrev32ifbe(is->length);
    int prepend = value < 0 ? 1 : 0;

    //根據新的元素記憶體大小重新分配 intset 記憶體大小
    is->encoding = intrev32ifbe(newenc);
    is = intsetResize(is,intrev32ifbe(is->length)+1);
    //這個地方我先標記一下 @1,下面詳細分析
    //總體上你可以理解,就是我們上圖畫的那樣,從原集合的最後一個元素
    //開始擴大它佔用的位元位
    while(length--)
        _intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc));

    //將新元素放進 intset 中
    if (prepend)
        _intsetSet(is,0,value);
    else
        _intsetSet(is,intrev32ifbe(is->length),value);
    is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
    return is;
}

別的不再解釋,我重點解釋一下我做標記的 @1,這個迴圈其實是這個方法的核心點,它完成了將舊元素擴充位元位這麼一個操作。

首先明確的一點是,升級操作只有兩種情況會觸發,一種是新插入一個較大的數值,另一種是新插入一個負很大的值,這兩種情況都會導致型別不夠儲存,需要擴大資料位。

_intsetGetEncoded 這個方法可以根據給定了 length,也就是元素在陣列中的下標取出舊陣列中對應的元素,很顯然,這裡是從後往前倒著來的。

因為我們的 intsetResize 方法已經完成了擴容記憶體的操作,也就是說新元素的記憶體已經分配完畢,那麼 _intsetSet 方法就會將 _intsetGetEncoded 取出的元素重新的向陣列中賦值。迴圈結束時,就是所有元素重新歸位的時候,最後再將新元素賦值進入陣列最後的位置。

但其實細心的同學會發現,_intsetSet 方法在傳下標索引的時候實際傳的是 length+prepend,這其實就是我們說,如果 value 是小於零的,length+prepend 最終會導致所有的舊元素往後挪了一個偏移量,然後新的元素會被賦值的索引為零的位置。也就是說,如果新插入的數值是負數,它會被頭插進陣列的第一個位置。

核心的幾個 API 我們都已經介紹了,其他的一些 API 你可以自行參閱原始碼,相信對你不難。

總結一下,整數集合(intset)使用了非常簡潔的資料結構,可以更少的佔用記憶體儲存一些整數,但終究是基於陣列的,也就避免不了不能儲存大量資料的缺點。總體來說,插入一個元素,最好情況 O(logN),最壞的情況是 O(N),攤還時間複雜度為 O(N),查詢一個元素,根據索引下標時間複雜度在 O(1)。當 intset 中的元素超過 512 個,或者向其中添加了字串,redis 會將 intset 轉換成字典。

同樣的,如果覺得我寫的對你有點幫助的話,順手點一波關注吧,也歡迎加作者微信深入探討,我們下一講,壓縮列表,盡請關注。


關注公眾不迷路,一個愛分享的程式設計師。

公眾號回覆「1024」加作者微信一起探討學習!

每篇文章用到的所有案例程式碼素材都會上傳我個人 github

https://github.com/SingleYam/overview_java

歡迎來踩!

相關推薦

Redis底層資料結構整數集合

當一個集合中只包含整數,並且元素的個數不是很多的話,redis 會用整數集合作為底層儲存,它的一個優點就是可以節省很多記憶體,雖然字典結構的效率很高,但是它的實現結構相對複雜並且會分配較多的記憶體空間。 而我們的整數集合(intset)可以做到使用較少的記憶體空間卻達到和字典一樣效率的實現,但也是前提的,集合

Redis底層資料結構跳躍表

字典相對於陣列,連結串列來說,是一種較高層次的資料結構,像我們的漢語字典一樣,可以通過拼音或偏旁唯一確定一個漢字,在程式裡我們管每一個對映關係叫做一個鍵值對,很多個鍵值對放在一起就構成了我們的字典結構。 有很多高階的字典結構實現,例如我們 Java 中的 HashMap 底層實現,根據鍵的 Hash 值均勻的

Redis底層資料結構壓縮列表

上一篇我們介紹了 redis 中的整數集合這種資料結構的實現,也談到了,引入這種資料結構的一個很大的原因就是,在某些僅有少量整數元素的集合場景,通過整數集合既可以達到字典的效率,也能使用遠少於字典的記憶體達到同樣的效果。 我們本篇介紹的壓縮列表,相信你從他的名字裡應該也能看出來,又是一個為了節約記憶體而設計的

Redis底層資料結構SDS和連結串列

Redis 是一個開源(BSD許可)的,記憶體中的資料結構儲存系統,它可以用作資料庫、快取和訊息中介軟體。可能幾乎所有的線上專案都會使用到 Redis,無論你是做快取、或是用作訊息中介軟體,用起來很簡單方便,但可能大多數人並沒有去深入底層的看看 Redis 的一些策略實現等等細節。 正好最近也在專案開發中遇到

Redis底層資料結構字典

字典相對於陣列,連結串列來說,是一種較高層次的資料結構,像我們的漢語字典一樣,可以通過拼音或偏旁唯一確定一個漢字,在程式裡我們管每一個對映關係叫做一個鍵值對,很多個鍵值對放在一起就構成了我們的字典結構。 有很多高階的字典結構實現,例如我們 Java 中的 HashMap 底層實現,根據鍵的 Hash 值均勻的

Redis底層資料結構物件

目前為止,我們介紹了 redis 中非常典型的五種資料結構,從 SDS 到 壓縮列表,這都是 redis 最底層、最常用的資料結構,相信你也掌握的不錯。 但 redis 實際儲存鍵值對的時候,是基於物件這個基本單位的,並且往往一個物件下面對對應不同的底層資料結構實現以便於在不同的場景下切換底層實現提升效率。例

redis 系列8 資料結構整數集合

一.概述   整數集合(intset)是集合鍵的底層實現之一, 當一個集合只包含整數值元素,並且這個集合元素數量不多時, Redis就會使用整數集合作為集合鍵的底層實現。下面建立一個只包含5個元素的集合鍵,並且集合中所有元素都是整數值,那麼這個集合鍵的底層實現就會是整數集合。 接著新增非整數值,集合鍵的底層

Redis 基礎資料結構

1.String字串 string 是 Redis 最簡單的資料結構。Redis 所有的資料結構都是以唯一的 key 字串作為名稱,然後通過這個唯一 key 值來獲取相應的 value 資料。不同型別的資料結構的差異就在於 value 的結構不一樣。字串結構使用最為廣泛,最常見的就是快取資訊。一般情況下我們是

redis基礎資料結構 基數統計

基數統計即統計一個數據集中不重複元素的個數,一種顯然的實現是使用不相交集,缺陷是隨著資料增加記憶體佔用線性增加,海量資料下不可用;一種更常見的方法是使用B-樹,所有資料在葉子節點儲存,葉子節點在磁碟中,上層節點在記憶體中,因此佔用記憶體的問題得到解決,查詢時間O(logN),

Redis資料結構整數集合

# 1、整數集合 Redis 中有集合(set)的操作,常用的指令有 **SADD、SCARD 等**,而在底層的實現中,整數集合(intset)就是 Redis 集合的實現方式之一。 Redis 的集合是有序集合,intset 也是有序的。 根據 Redis 對集合的操作,我們可以大致想象出,ints

Redis數據結構整數集合

red bubuko 底層 不支持 style 示例 類型轉換 新元素 包含   整數集合是Redis用於保存整數值的集合抽象數據結構,它可以保存類型為int16_t 、int32_t或者int64_t的整數值,並且保證集合中不會出現重復元素。 一、整數集合數據結構定義

資料結構c語言——鏈棧儲存結構及實現

鏈棧:就是一種特殊的單鏈表,FILO(先進後出) 通常對於連結串列來說: 1.是不需要頭節點的。 2.基本不存在滿棧的情況,除非記憶體已沒有可用的空間。   不多bibi你們都懂哈,直接上程式碼:  鏈棧結構: typedef struct Stock

資料結構c語言——雙向連結串列的基本操作

定義一個雙向連結串列結構: typedef struct DulNode{ // *prior:前一個元素的地址 // *next:後一個元素的地址 struct DulNode *prior; Element data; struct DulNode *

Redis底層資料結構--SDS

這是一種用於儲存二進位制資料的一種結構, 具有動態擴容的特點. 其實現位於src/sds.h與src/sds.c中, 其關鍵定義如下: typedef char *sds; /* Note: sdshdr5 is never used, we just access the flags byt

Redis底層資料結構--連結串列

這是普通的連結串列實現, 連結串列結點不直接持有資料, 而是通過void *指標來間接的指向資料. 其實現位於 src/adlist.h與src/adlist.c中, 關鍵定義如下: typedef struct listNode { struct listNode *prev

Redis底層資料結構dict

dict是Redis底層資料結構中實現最為複雜的一個數據結構, 其功能類似於C++標準庫中的std::unordered_map, 其實現位於 src/dict.h 與 src/dict.c中, 其關鍵定義如下: typedef struct dictEntry {

資料結構十五排序

排序演算法 0. 前言 本來準備自己寫,無意間看到一位大佬的博文…大家還是移步吧 推薦一套自己開發的演算法演示工具 1. 總結 排序方法 平均時間複雜度 最壞時間複雜度 額外空間複雜度 穩定性

資料結構十五最小生成樹

最小生成樹問題 1. 什麼是最小生成樹 是一棵樹 無迴路 |V|個頂點一定有 |V|-1 條邊 是生成樹 不唯一 包含全部頂點 |V|-1 條邊都在圖裡 邊的權值和最小

資料結構十四最短路問題

最短路徑問題 1. 概述 1. 抽象 在網路(帶權圖)中,求兩個不同頂點之間的所有路徑中,邊的權值之和最小的那一條路徑 這條路徑就是兩點之間的最短路徑(ShorttestPath) 第一個頂點為源點(Source) 最後一個頂點為終點(Destinatio

資料結構非線性表

非線性結構-樹 實驗簡介 前面兩章我們講解了資料結構中的線性結構--線性表、棧和佇列,這章開始以及下一章我們將講解非線性結構樹和圖。 一、樹 什麼是樹呢?樹很好地反應了一種層次結構,例如下圖,這就是一種樹形結構,它有很多結點組成,最上面的實驗樓課程結點稱為樹的根,結點擁