Redis SCAN命令實現有限保證的原理

来源:https://www.cnblogs.com/linxiyue/archive/2019/07/29/11262969.html
-Advertisement-
Play Games

SCAN命令可以為用戶保證:從完整遍歷開始直到完整遍歷結束期間,一直存在於數據集內的所有元素都會被完整遍歷返回,但是同一個元素可能會被返回多次。如果一個元素是在迭代過程中被添加到數據集的,又或者是在迭代過程中從數據集中被刪除的,那麼這個元素可能會被返回,也可能不會返回。 這是如何實現的呢,先從Red ...


SCAN命令可以為用戶保證:從完整遍歷開始直到完整遍歷結束期間,一直存在於數據集內的所有元素都會被完整遍歷返回,但是同一個元素可能會被返回多次。如果一個元素是在迭代過程中被添加到數據集的,又或者是在迭代過程中從數據集中被刪除的,那麼這個元素可能會被返回,也可能不會返回。

這是如何實現的呢,先從Redis中的字典dict開始。Redis的資料庫是使用dict作為底層實現的。

字典數據類型

Redis中的字典由dict.h/dict結構表示:

typedef struct dict {
    dictType *type;
    void *privdata;
    dictht ht[2];
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */
    unsigned long iterators; /* number of iterators currently running */
} dict;

typedef struct dictht {
    dictEntry **table;
    unsigned long size;
    unsigned long sizemask;
    unsigned long used;
} dictht;

字典由兩個哈希表dictht構成,主要用做rehash,平常主要使用ht[0]哈希表。

哈希表由一個成員為dictEntry的數組構成,size屬性記錄了數組的大小,used屬性記錄了已有節點的數量,sizemask屬性的值等於size - 1。數組大小一般是2n,所以sizemask二進位是0b11111...,主要用作掩碼,和哈希值一起決定key應該放在數組的哪個位置。

求key在數組中的索引的計算方法如下:

index = hash & d->ht[table].sizemask; 

也就是根據掩碼求低位值。

rehash的問題

字典rehash時會使用兩個哈希表,首先為ht[1]分配空間,如果是擴展操作,ht[1]的大小為第一個大於等於2倍ht[0].used的2n,如果是收縮操作,ht[1]的大小為第一個大於等於ht[0].used的2n。然後將ht[0]的所有鍵值對rehash到ht[1]中,最後釋放ht[0],將ht[1]設置為ht[0],新創建一個空白哈希表當做ht[1]。rehash不是一次完成的,而是分多次、漸進式地完成。

舉個例子,現在將一個size為4的哈希表ht[0](sizemask為11, index = hash & 0b11)rehash至一個size為8的哈希表ht[1](sizemask為111, index = hash & 0b111)。

ht[0]中處於bucket0位置的key的哈希值低兩位為00,那麼rehash至ht[1]時index取低三位可能為000(0)100(4)。也就是ht[0]中bucket0中的元素rehash之後分散於ht[1]的bucket0與bucket4,以此類推,對應關係為:

    ht[0]  ->  ht[1]
    ----------------
      0    ->   0,4 
      1    ->   1,5
      2    ->   2,6
      3    ->   3,7

如果SCAN命令採取0->1->2->3的順序進行遍歷,就會出現如下問題:

  • 擴展操作中,如果返回游標1時正在進行rehash,ht[0]中的bucket0中的部分數據可能已經rehash到ht[1]中的bucket[0]或者bucket[4],在ht[1]中從bucket1開始遍歷,遍歷至bucket4時,其中的元素已經在ht[0]中的bucket0中遍歷過,這就產生了重覆問題。
  • 縮小操作中,當返回游標5,但縮小後哈希表的size只有4,如何重置游標?

SCAN的遍歷順序

SCAN命令的遍歷順序,可以舉一個例子看一下:

127.0.0.1:6379[3]> keys *
1) "bar"
2) "qux"
3) "baz"
4) "foo"
127.0.0.1:6379[3]> scan 0 count 1
1) "2"
2) 1) "bar"
127.0.0.1:6379[3]> scan 2 count 1
1) "1"
2) 1) "foo"
127.0.0.1:6379[3]> scan 1 count 1
1) "3"
2) 1) "qux"
   2) "baz"
127.0.0.1:6379[3]> scan 3 count 1
1) "0"
2) (empty list or set)

可以看出順序是0->2->1->3,很難看出規律,轉換成二進位觀察一下:

00 -> 10 -> 01 -> 11

二進位就很明瞭了,遍歷採用的順序也是加法,但每次是高位加1的,也就是從左往右相加、從高到低進位的。

SCAN源碼

SCAN遍歷字典的源碼在dict.c/dictScan,分兩種情況,字典不在進行rehash或者正在進行rehash。

不在進行rehash時,游標是這樣計算的:

m0 = t0->sizemask;

// 將游標的umask位的bit都置為1
v |= ~m0;

// 反轉游標
v = rev(v);
// 反轉後+1,達到高位加1的效果
v++;
// 再次反轉複位
v = rev(v);

當size為4時,sizemask為3(00000011),游標計算過程:

         v |= ~m0    v = rev(v)    v++       v = rev(v)

00000000(0) -> 11111100 -> 00111111 -> 01000000 -> 00000010(2)

00000010(2) -> 11111110 -> 01111111 -> 10000000 -> 00000001(1)

00000001(1) -> 11111101 -> 10111111 -> 11000000 -> 00000011(3)

00000011(3) -> 11111111 -> 11111111 -> 00000000 -> 00000000(0)

遍歷size為4時的游標狀態轉移為0->2->1->3

同理,size為8時的游標狀態轉移為0->4->2->6->1->5->3->7,也就是000->100->010->110->001->101->011->111

再結合前面的rehash:

    ht[0]  ->  ht[1]
    ----------------
      0    ->   0,4 
      1    ->   1,5
      2    ->   2,6
      3    ->   3,7

可以看出,當size由小變大時,所有原來的游標都能在大的哈希表中找到相應的位置,並且順序一致,不會重覆讀取並且不會遺漏。

當size由大變小的情況,假設size由8變為了4,分兩種情況,一種是游標為0,2,1,3中的一種,此時繼續讀取,也不會遺漏和重覆。

但如果游標返回的不是這四種,例如返回了7,7&11之後變為了3,所以會從size為4的哈希表的bucket3開始繼續遍歷,而bucket3包含了size為8的哈希表中的bucket3與bucket7,所以會造成重覆讀取size為8的哈希表中的bucket3的情況。

所以,redis里rehash從小到大時,SCAN命令不會重覆也不會遺漏。而從大到小時,有可能會造成重覆但不會遺漏。

當正在進行rehash時,游標計算過程:

        /* Make sure t0 is the smaller and t1 is the bigger table */
        if (t0->size > t1->size) {
            t0 = &d->ht[1];
            t1 = &d->ht[0];
        }

        m0 = t0->sizemask;
        m1 = t1->sizemask;

        /* Emit entries at cursor */
        if (bucketfn) bucketfn(privdata, &t0->table[v & m0]);
        de = t0->table[v & m0];
        while (de) {
            next = de->next;
            fn(privdata, de);
            de = next;
        }

        /* Iterate over indices in larger table that are the expansion
         * of the index pointed to by the cursor in the smaller table */
        do {
            /* Emit entries at cursor */
            if (bucketfn) bucketfn(privdata, &t1->table[v & m1]);
            de = t1->table[v & m1];
            while (de) {
                next = de->next;
                fn(privdata, de);
                de = next;
            }

            /* Increment the reverse cursor not covered by the smaller mask.*/
            v |= ~m1;
            v = rev(v);
            v++;
            v = rev(v);

            /* Continue while bits covered by mask difference is non-zero */
        } while (v & (m0 ^ m1));

演算法會保證t0是較小的哈希表,不是的話t0與t1互換,先遍歷t0中游標所在的bucket,然後再遍歷較大的t1。

求下一個游標的過程基本相同,只是把m0換成了rehash之後的哈希表的m1,同時還加了一個判斷條件:

v & (m0 ^ m1)

size4的m0為00000011,size8的m1為00000111m0 ^ m1取值為00000100,即取二者mask的不同位,看游標在這些標誌位是否為1。

假設游標返回了2,並且正在進行rehash,此時size由4變成了8,二者mask的不同位是低第三位。

首先遍歷t0中的bucket2,然後遍歷t1中的bucket2,公式計算出的下一個游標為6(00000110),低第三位為1,繼續迴圈,遍歷t1中的bucket6,然後計算游標為1,結束迴圈。

所以正在rehash時,是兩個哈希表都遍歷的,以避免遺漏的情況。


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

-Advertisement-
Play Games
更多相關文章
  • 用戶管理: 1、新建用戶: CREATE USER name IDENTIFIED BY 'zhangsan' 2、更改密碼: SET PASSWORD FOR name=PASSWORD('zhangsan123') 3、許可權管理: //查看name用戶許可權 SHOW GRANTS FOR nam ...
  • 一、資料庫對象: 模式對象: 資料庫對象是邏輯結構的集合,最基本的資料庫對象是表; 其他對象包括:create增、drop刪、改alter 同義詞、序列、視圖、索引 1、同義詞: ①、 現有對象的一個別名: 簡化SQL語句,隱藏對象的名稱和所有者,提供對對象的公共訪問; ②、類型: 私有同義詞: 只 ...
  • 一、操作符: 1、分類: 算術、比較、邏輯、集合、連接; 2、算術操作符: 執行數值計算; 3、比較操作符: (不等於是!= , 在mysql中是<> ) 4、邏輯操作符:and or not 5、集合操作符: 將兩個查詢的結果組合成一個結果: ①、intersect 返回兩個查詢的公共行; ②、u ...
  • 1.項目背景 因監控需要,我們需要在既有的每個MySQL實例上創建一個賬號。公司有數百台 MySQL 實例,如果手動登入來創建賬號很麻煩,也不現實。所以,我們寫了一個簡單的shell腳本,用來創建批量伺服器的mysql 賬號。 2.執行腳本內容; 3. 執行舉例 Step 1 將代碼放置到執行文件中 ...
  • 一、Spark提交應用任務的四個階段: 總共提交的任務分為四個階段,提交+執行: 1、在分配完畢executor以後,解析代碼生成DAG有向無環圖; 2、將生成的DAG圖提交給DAGScheduler,這個組件在driver內,DAGScheduler負責切分階段,按照DAG圖中的shuffle運算元 ...
  • 一、單表簡單查詢: 1、 2、去重: 3、別名: 4、排序: 5、模糊查詢: 二、多表連接查詢: 1、交叉連接:若查詢共有欄位,需要制定該欄位來自哪個表格; 自然連接 2、外連接: 三、分組聚合: 1、group by: 2、 where 放在group 之前,分組之後條件用having; 四、子查 ...
  • 業務實體,客戶名稱,訂單類型,價目表,業務員,付款條件,客戶採購定訂單,發貨組織,訂購項目,訂購數量,單位,單價,稅分類代碼 上面這個是導入的列 PROCEDURE IMPORT_SALE_ORDER AS CURSOR SALES_LINES_CURSOR IS SELECT ID, TRIM(C ...
  • 我們今天來進行建表的基本操作: 首先要建表就要瞭解列類型,因為建表就是聲明列的過程,列聲明完成了,表也就建好了。 mysql中列分為三大類: 一、數值型 數值型又分為整型和浮點型兩種。 先來看整型: tinyint:占據空間:1個位元組;存儲範圍:帶符號數:-2^7(-128)~2^7-1(127), ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...