[Redis源碼閱讀]dict字典的實現

来源:https://www.cnblogs.com/hoohack/archive/2018/01/08/8241665.html
-Advertisement-
Play Games

dict是一種用於保存鍵值對的抽象數據結構,在redis中使用非常廣泛,比如資料庫、哈希結構的底層。 ...


dict的用途

dict是一種用於保存鍵值對的抽象數據結構,在redis中使用非常廣泛,比如資料庫、哈希結構的底層。

當執行下麵這個命令:

> set msg "hello"

以及使用哈希結構,如:

> hset people name "hoohack"

都會使用到dict作為底層數據結構的實現。

結構的定義

先看看字典以及相關數據結構體的定義:

字典

/* 字典結構 每個字典有兩個哈希表,實現漸進式哈希時需要用在將舊表rehash到新表 */
typedef struct dict {
    dictType *type; /* 類型特定函數 */
    void *privdata; /* 保存類型特定函數需要使用的參數 */
    dictht ht[2]; /* 保存的兩個哈希表,ht[0]是真正使用的,ht[1]會在rehash時使用 */
    long rehashidx; /* rehashing not in progress if rehashidx == -1 rehash進度,如果不等於-1,說明還在進行rehash */
    unsigned long iterators; /* number of iterators currently running 正在運行中的遍歷器數量 */
} dict;

哈希表

/* 哈希表結構 */
typedef struct dictht {
    dictEntry **table; /* 哈希表節點數組 */
    unsigned long size; /* 哈希表大小 */
    unsigned long sizemask; /* 哈希表大小掩碼,用於計算哈希表的索引值,大小總是dictht.size - 1 */
    unsigned long used; /* 哈希表已經使用的節點數量 */
} dictht;

哈希表節點

/* 哈希表節點 */
typedef struct dictEntry {
    void *key; /* 鍵名 */
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v; /* 值 */
    struct dictEntry *next; /* 指向下一個節點, 將多個哈希值相同的鍵值對連接起來*/
} dictEntry;

dictType

/* 保存一連串操作特定類型鍵值對的函數 */
typedef struct dictType {
    uint64_t (*hashFunction)(const void *key); /* 哈希函數 */
    void *(*keyDup)(void *privdata, const void *key); /* 複製鍵函數 */
    void *(*valDup)(void *privdata, const void *obj); /* 複製值函數 */
    int (*keyCompare)(void *privdata, const void *key1, const void *key2); /* 比較鍵函數 */
    void (*keyDestructor)(void *privdata, void *key); /* 銷毀鍵函數 */
    void (*valDestructor)(void *privdata, void *obj); /* 銷毀值函數 */
} dictType;

把上面的結構定義串起來,得到下麵的字典數據結構:
dict struct

根據數據結構定義,把關聯圖畫出來後,看代碼的時候就更加清晰。

從圖中也可以看出來,字典的哈希表裡,使用了鏈表解決鍵衝突的情況,稱為鏈式地址法。

rehash(重新散列)

當操作越來越多,比如不斷的向哈希表添加元素,此時哈希表需要分配了更多的空間,如果接下來的操作是不斷地刪除哈希表的元素,那麼哈希表的大小就會發生變化,更重要的是,現在的哈希表不再需要那麼大的空間了,在redis的實現中,為了保證哈希表的負載因數維持在一個合理範圍內,當哈希表保存的鍵值對太多或者太少時,redis對哈希表大小進行相應的擴展和收縮,稱為rehash(重新散列)。

執行rehash的流程圖

redis dict rehash

負載因數解釋

負載因數 = 哈希表已保存節點數量 / 哈希表大小

負載因數越大,意味著哈希表越滿,越容易導致衝突,性能也就越低。因此,一般來說,當負載因數大於某個常數(可能是 1,或者 0.75 等)時,哈希表將自動擴容。

漸進式rehash

在上面的rehash流程圖裡面,rehash的操作不是一次性就完成了的,而是分多次,漸進式地完成。

原因是,如果需要rehash的鍵值對較多,會對伺服器造成性能影響,漸進式地rehash避免了對伺服器的影響。

漸進式的rehash使用了dict結構體中的rehashidx屬性輔助完成。當漸進式哈希開始時,rehashidx會被設置為0,表示從dictEntry[0]開始進行rehash,每完成一次,就將rehashidx加1。直到ht[0]中的所有節點都被rehash到ht[1],rehashidx被設置為-1,此時表示rehash結束。

結合代碼再深入理解

/* 實現漸進式的重新哈希,如果還有需要重新哈希的key,返回1,否則返回0
 *
 * 需要註意的是,rehash持續將bucket從老的哈希表移到新的哈希表,但是,因為有的哈希表是空的,
 * 因此函數不能保證即使一個bucket也會被rehash,因為函數最多一共會訪問N*10個空bucket,不然的話,函數將會耗費過多性能,而且函數會被阻塞一段時間
 */
int dictRehash(dict *d, int n) {
    int empty_visits = n*10; /* Max number of empty buckets to visit. */
    if (!dictIsRehashing(d)) return 0;

    while(n-- && d->ht[0].used != 0) {
        dictEntry *de, *nextde;

        assert(d->ht[0].size > (unsigned long)d->rehashidx);
        /* 找到非空的哈希表下標 */
        while(d->ht[0].table[d->rehashidx] == NULL) {
            d->rehashidx++;
            if (--empty_visits == 0) return 1;
        }
        de = d->ht[0].table[d->rehashidx];
        
        /* 實現將bucket從老的哈希表移到新的哈希表 */
        while(de) {
            unsigned int h;

            nextde = de->next;
            /* Get the index in the new hash table */
            h = dictHashKey(d, de->key) & d->ht[1].sizemask;
            de->next = d->ht[1].table[h];
            d->ht[1].table[h] = de;
            d->ht[0].used--;
            d->ht[1].used++;
            de = nextde;
        }
        d->ht[0].table[d->rehashidx] = NULL;
        d->rehashidx++;
    }

    /* 如果已經完成了,釋放舊的哈希表,返回0 */
    if (d->ht[0].used == 0) {
        zfree(d->ht[0].table);
        d->ht[0] = d->ht[1];
        _dictReset(&d->ht[1]);
        d->rehashidx = -1;
        return 0;
    }

    /* 繼續下一次rehash */
    return 1;
}

在漸進式rehash期間,所有對字典的操作,包括:添加、查找、更新等等,程式除了執行指定的操作之外,還會順帶將ht[0]哈希表索引的所有鍵值對rehash到ht[1]。比如添加:

dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing)
{
    int index;
    dictEntry *entry;
    dictht *ht;

    /* 如果正在rehash,順帶執行rehash操作 */
    if (dictIsRehashing(d)) _dictRehashStep(d);

    /* 獲取新元素的下標,如果已經存在,返回-1 */
    if ((index = _dictKeyIndex(d, key, dictHashKey(d,key), existing)) == -1)
        return NULL;

    ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0]; // 如果正在進行rehash操作,返回ht[1],否則返回ht[0]
    entry = zmalloc(sizeof(*entry));
    entry->next = ht->table[index];
    ht->table[index] = entry;
    ht->used++;

    /* Set the hash entry fields. */
    dictSetKey(d, entry, key);
    return entry;
}

總結

使用一個標記值標記某項操作正在執行是編程中常用的手段,比如本文提到的rehashidx,多利用此手段可以解決很多問題。

我在github有對Redis源碼更詳細的註解。感興趣的可以圍觀一下,給個star。Redis4.0源碼註解。可以通過commit記錄查看已添加的註解。

原創文章,文筆有限,才疏學淺,文中若有不正之處,萬望告知。

更多精彩內容,請關註個人公眾號。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • oracle 序列的創建與使用 (2012-03-15 16:14:09) 轉載 oracle 序列的創建與使用 轉載 在Oracle中,可以使用序列自動生成一個整數序列,主要用來自動為表中的數據類型的主鍵列提供有序的唯一值,這樣就可以避免在向表中添加數據時,手工指定主鍵值。而且使用手工指定主鍵值這 ...
  • 1.1 前言 在進行MySQL的優化之前必須要瞭解的就是MySQL的查詢過程,很多的查詢優化工作實際上就是遵循一些原則讓MySQL的優化器能夠按照預想的合理方式運行而已。更多關於MySQL查詢相關參照:http://www.cnblogs.com/clsn/p/8038964.html#_label ...
  • 學習目標 -瞭解分析函數作用和類型 -使用分析函數產生報告 分析函數 分析函數用於計算一些基於組的聚合值,它與聚合函數的區別在於,分析函數每組返回多行,聚合函數每組返回一行。 一般分析函數 ROW_NUMBER() OVER(PARTITION BY ... ORDER BY ...) 按分區或返回 ...
  • 查詢出重覆記錄 select * from 重覆記錄欄位 in ( select 重覆記錄欄位 form 數據表 group by 重覆記錄欄位 having count(重覆記錄欄位)>1) ...
  • 應儘量避免在where中使用!=或<>操作符。否則會進行全表查詢 對於查詢,避免全盤掃描,考慮在where或order by涉及到的列上建立索引 避免在where中進行null值判斷,否則會進行全表掃描 查詢時,避免*查詢全部,按要求指定的查 In和not in也要慎用,否則會導致全表掃描 不要寫一 ...
  • 針對mysql的連接參數和狀態值,本文做些介紹和對比 一、MYSQL連接參數變數 1、常用連接數限制參數 show variables like '%connect%'; 2、超時參數 mysql -e "show variables like '%timeout%'" 二、MySQL連接狀態變數 ...
  • 需求說明: 1、在項目中需要計算某一個環節的持續時間及該環節進行的次數。 2、要求持續時間以分鐘進行顯示,並統計進行次數。 解決方式: 通過計算某一環節的開始時間與結束時間的秒數差值進行判斷。 代碼部分: 說明:資料庫使用的是Mysql,持久層框架使用的是Mybatis。 代碼如下: FLOOR(( ...
  • 在windows安裝好了windows,首先記得要把mongodb bin目錄路徑放在 系統環境變數的path中,確定之後即配置好了mongo的環境變數,在dos命令框中輸入mongo會出現如下 版本信息: 想要啟動本地mongo 服務,直接在命令框中輸入 mongod.exe 即可啟動 mongo ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...