Redis:Bitmap 實現億萬級數據計算

1 前言

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

除了這常見數據類型,還有一些不常用的數據類型,如 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);

則具體體現爲:

QIEpyP

可以看出用戶 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 表示。

SETBIT online_statu 1024 1
GETBIT online_statu 1024
SETBIT online_statu 1024 0
基於上面的討論,我們可以總結出一個預評估公式,根據實際的數據量獲取存儲空間:( offset / 8 / 1024 / 1024 ) M

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

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

# 22年第一天
SETBIT sign_1024_2022 0 1

# 22年最後一天
SETBIT sign_1024_2022 364 1
GETBIT sign_1024_2022 150
BITCOUNT sign_1024_2022
$index = BITPOS sign_1024_2022 30

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

3.3 連續簽到用戶信息

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

如果這時候我們要判斷用戶是否整週都有簽到或者整個月都有簽到就可以使用 【與】運算
只有指定週期內的所有值都是 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 這四種操作中的任意一種:

除了 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 用戶統計 等等場景,但凡涉及到二值狀態識別、海量統計的數據都可以考慮使用。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/L9_UJlh3RYz6jmlmE3tOSw