天天看點

常見業務&高頻面試題:Redis Bitmap 實作億級海量資料統計

在移動應用的業務場景中,我們需要儲存這樣的資訊:一個 key 關聯了一個資料集合。

常見的場景如下:

  • 給一個 userId ,判斷使用者登陸狀态;
  • 顯示使用者某個月的簽到次數和首次簽到時間;
  • 兩億使用者最近 7 天的簽到情況,統計 7 天内連續簽到的使用者總數;

通常情況下,我們面臨的使用者數量以及通路量都是巨大的,比如百萬、千萬級别的使用者數量,或者千萬級别、甚至億級别的通路資訊。

是以,我們必須要選擇能夠非常高效地統計大量資料(例如億級)的集合類型。

如何選擇合适的資料集合,我們首先要了解常用的統計模式,并運用合理的資料類型來解決實際問題。

四種統計類型:

  1. 二值狀态統計;
  2. 聚合統計;
  3. 排序統計;
  4. 基數統計。

本文将由二值狀态統計類型作為實戰篇系列的開篇,文中将用到 ​​String、Set、Zset、List、hash​​​ 以外的拓展資料類型 ​

​Bitmap​

​ 來實作。

文章涉及到的指令可以通過線上 Redis 用戶端運作調試,位址:https://try.redis.io/,超友善的說。

二值狀态統計

什麼是二值狀态統計呀?

也就是集合中的元素的值隻有 0 和 1 兩種,在簽到打卡和使用者是否登陸的場景中,隻需記錄​

​簽到(1)​

​​或 ​

​未簽到(0)​

​​,​

​已登入(1)​

​​或​

​未登陸(0)​

​。

假如我們在判斷使用者是否登陸的場景中使用 Redis 的 String 類型實作(key -> userId,value -> 0 表示下線,1 - 登陸),假如存儲 100 萬個使用者的登陸狀态,如果以字元串的形式存儲,就需要存儲 100 萬個字元串了,記憶體開銷太大。

為什麼 String 類型記憶體開銷大?

String 類型除了記錄實際資料以外,還需要額外的記憶體記錄資料長度、空間使用等資訊。

當儲存的資料包含字元串,String 類型就使用簡單動态字元串(SDS)結構體來儲存,如下圖所示:

常見業務&高頻面試題:Redis Bitmap 實作億級海量資料統計

SDS

  • len:占 4 個位元組,表示 buf 的已用長度。
  • alloc:占 4 個位元組,表示 buf 實際配置設定的長度,通常 > len。
  • buf:位元組數組,儲存實際的資料,Redis 自動在數組最後加上一個 “\0”,額外占用一個位元組的開銷。

是以,在 SDS 中除了 buf 儲存實際的資料, len 與 alloc 就是額外的開銷。

另外,還有一個 RedisObject 結構的開銷,因為 Redis 的資料類型有很多,而且,不同資料類型都有些相同的中繼資料要記錄(比如最後一次通路的時間、被引用的次數等)。

是以,Redis 會用一個 RedisObject 結構體來統一記錄這些中繼資料,同時指向實際資料。

常見業務&高頻面試題:Redis Bitmap 實作億級海量資料統計

對于二值狀态場景,我們就可以利用 Bitmap 來實作。比如登陸狀态我們用一個 bit 位表示,一億個使用者也隻占用 一億 個 bit 位記憶體 ≈ (100000000 / 8/ 1024/1024)12 MB。

大概的空間占用計算公式是:($offset/8/1024/1024) MB

      
什麼是 Bitmap 呢?

Bitmap 的底層資料結構用的是 String 類型的 SDS 資料結構來儲存位數組,Redis 把每個位元組數組的 8 個 bit 位利用起來,每個 bit 位 表示一個元素的二值狀态(不是 0 就是 1)。

可以将 Bitmap 看成是一個 bit 為機關的數組,數組的每個單元隻能存儲 0 或者 1,數組的下标在 Bitmap 中叫做 offset 偏移量。

為了直覺展示,我們可以了解成 buf 數組的每個位元組用一行表示,每一行有 8 個 bit 位,8 個格子分别表示這個位元組中的 8 個 bit 位,如下圖所示:

常見業務&高頻面試題:Redis Bitmap 實作億級海量資料統計

Bitmap

8 個 bit 組成一個 Byte,是以 Bitmap 會極大地節省存儲空間。 這就是 Bitmap 的優勢。

判斷使用者登陸态

怎麼用 Bitmap 來判斷海量使用者中某個使用者是否線上呢?

Bitmap 提供了 ​

​GETBIT、SETBIT​

​ 操作,通過一個偏移值 offset 對 bit 數組的 offset 位置的 bit 位進行讀寫操作,需要注意的是 offset 從 0 開始。

隻需要一個 key = login_status 表示存儲使用者登陸狀态集合資料, 将使用者 ID 作為 offset,線上就設定為 1,下線設定 0。通過 ​

​GETBIT​

​判斷對應的使用者是否線上。50000 萬 使用者隻需要 6 MB 的空間。

SETBIT 指令

SETBIT <key> <offset> <value>

      

設定或者清空 key 的 value 在 offset 處的 bit 值(隻能是 0 或者 1)。

GETBIT 指令

GETBIT <key> <offset>

      

擷取 key 的 value 在 offset 處的 bit 位的值,當 key 不存在時,傳回 0。

假如我們要判斷 ID = 10086 的使用者的登陸情況:

第一步,執行以下指令,表示使用者已登入。

SETBIT login_status 10086 1

      

第二步,檢查該使用者是否登陸,傳回值 1 表示已登入。

GETBIT login_status 10086

      

第三步,登出,将 offset 對應的 value 設定成 0。

SETBIT login_status 10086 0

      

使用者每個月的簽到情況

在簽到統計中,每個使用者每天的簽到用 1 個 bit 位表示,一年的簽到隻需要 365 個 bit 位。一個月最多隻有 31 天,隻需要 31 個 bit 位即可。

比如統計編号 89757 的使用者在 2021 年 5 月份的打卡情況要如何進行?

key 可以設計成 ​

​uid:sign:{userId}:{yyyyMM}​

​​,月份的每一天的值 - 1 可以作為 offset(因為 offset 從 0 開始,是以 ​

​offset = 日期 - 1​

​)。

第一步,執行下面指令表示記錄使用者在 2021 年 5 月 16 号打卡。

SETBIT uid:sign:89757:202105 15 1

      

第二步,判斷編号 89757 使用者在 2021 年 5 月 16 号是否打卡。

GETBIT uid:sign:89757:202105 15

      

第三步,統計該使用者在 5 月份的打卡次數,使用 ​

​BITCOUNT​

​ 指令。該指令用于統計給定的 bit 數組中,值 = 1 的 bit 位的數量。

BITCOUNT uid:sign:89757:202105

      

這樣我們就可以實作使用者每個月的打卡情況了,是不是很贊。

如何統計這個月首次打卡時間呢?

Redis 提供了 ​

​BITPOS key bitValue [start] [end]​

​​指令,傳回資料表示 Bitmap 中第一個值為 ​

​bitValue​

​ 的 offset 位置。

在預設情況下, 指令将檢測整個位圖, 使用者可以通過可選的 ​

​start​

​​ 參數和 ​

​end​

​ 參數指定要檢測的範圍。

是以我們可以通過執行以下指令來擷取 userID = 89757 在 2021 年 5 月份首次打卡日期:

BITPOS uid:sign:89757:202105 1

      

需要注意的是,我們需要将傳回的 value + 1 ,因為 offset 從 0 開始。

連續簽到使用者總數

在記錄了一個億的使用者連續 7 天的打卡資料,如何統計出這連續 7 天連續打卡使用者總數呢?

我們把每天的日期作為 Bitmap 的 key,userId 作為 offset,若是打卡則将 offset 位置的 bit 設定成 1。

key 對應的集合的每個 bit 位的資料則是一個使用者在該日期的打卡記錄。

一共有 7 個這樣的 Bitmap,如果我們能對這 7 個 Bitmap 的對應的 bit 位做 『與』運算。

同樣的 UserID  offset 都是一樣的,當一個 userID 在 7 個 Bitmap 對應對應的 offset 位置的 bit = 1 就說明該使用者 7 天連續打卡。

結果儲存到一個新 Bitmap 中,我們再通過 ​

​BITCOUNT​

​ 統計 bit = 1 的個數便得到了連續打卡 7 天的使用者總數了。

Redis 提供了 ​

​BITOP operation destkey key [key ...]​

​這個指令用于對一個或者多個 鍵 = key 的 Bitmap 進行位元操作。

​opration​

​​ 可以是 ​

​and​

​​、​

​OR​

​​、​

​NOT​

​​、​

​XOR​

​​。當 BITOP 處理不同長度的字元串時,較短的那個字元串所缺少的部分會被看作 ​

​0​

​​ 。空的 ​

​key​

​​ 也被看作是包含 ​

​0​

​ 的字元串序列。

便于了解,如下圖所示:

常見業務&amp;高頻面試題:Redis Bitmap 實作億級海量資料統計

BITOP

3 個 Bitmap,對應的 bit 位做「與」操作,結果儲存到新的 Bitmap 中。

操作指令表示将 三個 bitmap 進行 AND 操作,并将結果儲存到 destmap 中。接着對 destmap 執行 BITCOUNT 統計。

// 與操作
BITOP AND destmap bitmap:01 bitmap:02 bitmap:03
// 統計 bit 位 =  1 的個數
BITCOUNT destmap

      

小結

繼續閱讀