Redis系列8:Bitmap實現億萬級資料計算

2022-10-31 18:01:56

Redis系列1:深刻理解高效能Redis的本質
Redis系列2:資料持久化提高可用性
Redis系列3:高可用之主從架構
Redis系列4:高可用之Sentinel(哨兵模式)
Redis系列5:深入分析Cluster 叢集模式
追求效能極致:Redis6.0的多執行緒模型
追求效能極致:使用者端快取帶來的革命

1 前言

我們在第一篇 深刻理解高效能Redis的本質 的時候就介紹過Redis的幾種基本資料結構,它是基於不同業務場景而設計的:

  • 動態字串(REDIS_STRING):整數(REDIS_ENCODING_INT)、字串(REDIS_ENCODING_RAW)
  • 雙端列表(REDIS_ENCODING_LINKEDLIST)
  • 壓縮列表(REDIS_ENCODING_ZIPLIST)
  • 跳躍表(REDIS_ENCODING_SKIPLIST)
  • 雜湊表(REDIS_HASH)
  • 整數集合(REDIS_ENCODING_INTSET)

除了這常見資料型別,還有一些不常用的資料型別,如 BitMap、Geo、HyperLogLog 等等,他們在各自的領域為巨量資料量的統計,後面我們一一來介紹,學習下他們的實現原理和應用場景。

2 BitMap介紹

BitMap (點陣圖)的底層資料結構使用的是String型別的的 SDS 資料結構來儲存。因為一個位元組8個bit位,為了有效的將位元組的8個bit都利用到位,使用陣列模式儲存。
並且每個bit都使用二值狀態表示,要麼0,要麼1。
所以,BitMap 是通過一個 bit 位來表示某個元素對應的值或者狀態, 它的結構如下,key 對應元素本身;offset即是偏移量,固定整型,一般存陣列下表或者唯一值;value儲存的是二值(要麼0要麼1),一般用來表示狀態,如性別、是否登入、是否打卡等。

從上面可以看出這邊使用一個位元組表示1行,每1行儲存8個bit,就是可以儲存8個狀態位,極大的提高了空間利用。這也是BitMap的優勢,我們可以使用很少的位元組,儲存大量的線上狀態、打卡標記等狀態資訊,非常有效果。

我們可以使用 setbit, getbit, bitcount 等幾個相關命令來管理BitMap。語法如下:

SETBIT key offset value

上面說過了,key是元素名稱, offset 必須是數值型別,value 只能是 0 或者 1,如果我們儲存一個使用者的線上狀態,使用者,程式碼如下:

//設定線上狀態 
// $redis->setBit('online', $uid, 1);

$redis->setBit('online', 5, 1);
$redis->setBit('online', 9, 1);

則具體體現為:

byte bit0 bit1 bit2 bit3 bit4 bit5 bit6 bit7
buf[0] 0 0 0 0 0 1 0 0
buf[1] 0 1 0 0 0 0 0 0

可以看出使用者ID為5和9被打上1的標誌,代表線上狀態,其他未設定值預設為0,是離線狀態。
除了Set之外,還有getBit、bitCount等語法,如下:

// 獲取是否線上的狀態 
$isOnline = $redis->getBit('online', $uid); 
 
// 獲取線上人數 統計
$onlineNum = $redis->bitCount('online');

3 BitMap的主要應用場景

上面介紹了BitMap的原理和狀態儲存的優勢。那我們儲存了bit位,其實目的還是為了高效的計算,而不是簡單的狀態記錄。
而在實際的應用場景中,他主要解決如下幾個型別的需求:

3.1 狀態統計

上面其實我們已經演示過了,這種場景最常見,因為值只能是1或者0,所以所有的二值狀態的,所有存在是否對照關係的場景都可以使用。如線上(1) 離線(0),打卡(1) 未打卡(0),登入(1) 未登入(0),群聊訊息已閱(1) 未閱(0) 等等。
我們以使用者 離線/線上 為例子,看看如何使用 Bitmap 在海量的使用者資料之中判斷某個使用者是否是線上狀態。
假設我們使用一個 online_statu 來作為key,用來儲存 使用者登入後的狀態集合,而使用者的ID則為offset,online的狀態就用1表示,offline的狀態就用0表示。

  • 如果1024使用者登入系統,那麼設定ID為1024的使用者為線上的程式碼如下:
SETBIT online_statu 1024 1
  • 如果想看1024的使用者是否是線上狀態(這邊注意,key可能不存在,代表沒有這個使用者,這時候預設返回0),程式碼如下:
GETBIT online_statu 1024
  • 如果1024的使用者退出系統,則為他執行下線,程式碼如下:
SETBIT online_statu 1024 0
  • 空間上的有效利用,1億 人的狀態儲存只需要 100000000/8/1024/1024 = 11.92 M,簡單的資料結構也保證了效能上的優勢。
基於上面的討論,我們可以總結出一個預評估公式,根據實際的資料量獲取儲存空間:( offset / 8 / 1024 / 1024 ) M

3.2 固定週期的簽到情況統計(周/月/年)

固定週期可能是年/月/周,按照不同維度,可能有 365,31,7的bit位的統計週期。
假設這時候我們如果對於某個使用者(如1024)全年的簽到情況做統計,可以這麼設計:

  • 設計key 為 {bus_type}{uid}{yyyy} ,及業務型別+使用者id+年份
    比如 sign_1024_2022

  • 簽到則執行對應程式碼
    舉例,1024使用者在2022 年的第1天和最後一天如果有簽到,那就是:

# 22年第一天
SETBIT sign_1024_2022 0 1

# 22年最後一天
SETBIT sign_1024_2022 364 1
  • 判斷某使用者(1024)在某一天(150)是否有簽到
GETBIT sign_1024_2022 150
  • 統計某使用者(1024) 全面的簽到次數,使用 BITCOUNT 指令,統計給定的 bit 陣列中,值 = 1 的所有bit位數量。
BITCOUNT sign_1024_2022
  • 那如果你想限定範圍了怎麼辦,比如原來設計的是一年的統計。但是你想獲得某個月第一次打卡的資料,這時候就要使用BITPOS了。
    通過 BITPOS key value [start] [end] 指令,返回資料表示 Bitmap 中第一個值為 給定value 的 offset 位置。
    在預設情況下,命令會檢測整個點陣圖,但使用者也可以通過可選的start引數和end引數指定要檢測的範圍。
    比如第2個月的第3天是2月的第一次簽到日,則下面的返回結果為30(第一個月31天)+ 3(二月第3天簽到) = 33 :
$index = BITPOS sign_1024_2022 30

offset也是從0開始的,所以返回的值最好加個1,不會讓使用者看的暈頭轉向。

3.3 連續簽到使用者資訊

如果一個平臺有千萬級別以上的大量使用者,而我們需要統計每個使用者連續簽到的資訊,那需要怎麼設計呢?

  • 可以把每天的日期當成點陣圖(BitMap)的key,如 20221023
  • 使用者的唯一鍵當成(UserId)當成offset,如編號 1024 的使用者
  • 如果 1024 的使用者在 2022.10.23 有簽到,則點陣圖的value為1,否則為0。

如果這時候我們要判斷使用者是否整週都有簽到或者整個月都有簽到就可以使用 【與】運算
只有指定週期內的所有值都是1(簽到)的時候,結果才是1,否則是我們整週或者整個月都拿起來【與】運算,得到的結果是不是1就能確是否滿勤。

# 與運算:  0&0=0;0&1=0;1&0=0;1&1=1
# 下面為虛擬碼,類似:
(20221022 1024)  & ( 20221023 1024)  & ...

Redis 提供了 BITOP operation destkey key [key ...]這個指令用於對一個或者多個 鍵 = key 的 Bitmap 進行 位元 操作。
operation 可以是 AND 、 OR 、 NOT 、 XOR 這四種操作中的任意一種:

  • BITOP AND destkey key [key ...] ,對一個或多個 key 求邏輯並,並將結果儲存到 destkey 。
  • BITOP OR destkey key [key ...] ,對一個或多個 key 求邏輯或,並將結果儲存到 destkey 。
  • BITOP XOR destkey key [key ...] ,對一個或多個 key 求邏輯互斥或,並將結果儲存到 destkey 。
  • BITOP NOT destkey key ,對給定 key 求邏輯非,並將結果儲存到 destkey 。

除了 NOT 操作之外,其他操作都可以接受一個或多個 key 作為輸入。

# 統計一週的值(7個BitMap,10.17 ~ 10.23 號)並將結果存入到新的BitMap (sign-result) 中
redis> BITOP AND sign-result 20221017 20221018 20221019 20221020 20221021 20221022 20221023
(integer) 1

# 新的BitMap 中,獲取 1024的簽到結果,如果為1,則本週全部簽到
redis> GETBIT sign-result 1024
(integer) 1

可以理解下這張圖的運算過程:

這邊需要注意:當 BITOP 處理不同長度的字串時,較短字串所缺部分會被當作 0 對待。同樣的,空 key 也被看作是 0 的字串序列看待。

同理,類似HeapDump效能社群的使用者簽到統計,也可以用點陣圖(BitMap)這種方式計算!

小結

1個byte等於8個bit,每個bit位只使用0或者1來表示,這樣能夠有效的降低儲存空間,而Redis是儲存在快取記憶體中的,所以實際上是大大減少了記憶體佔用。
很多場景都可以使用點陣圖計算,比如我們上面說到的 是否登入、是否線上、是否簽到、使用者性別狀態、IP黑名單、是否VIP使用者統計 等等場景,但凡涉及到二值狀態識別、海量統計的資料都可以考慮使用。