「Redis」Redis Bitmap 「 位圖 還債篇 」

不積跬步,無以至千里;不積小流,無以成江海。

碼字不易,點贊再看。

前言

欠下的債總規要還,上篇介紹基礎數據結構中有提到 bitmap。移步至: 傳送門

本篇先從一道面試題開始,有這麼一個場景:擁有 數億 用戶的電商平臺,如何統計月活??

你用 set 記錄活躍用戶的id,沒毛病,確實也可以達到效果,但是使用這種數據結構真的妥當嗎??

針對如此寶貴的內存,能省點,不香嗎??

以 1億 用戶為例: 用戶id 用int存儲, 做個比對,引出今天的主角 bitmap。


「Redis」Redis Bitmap 「 位圖 還債篇 」

不用我說,已經看出來優勢了吧,沒錯這就是位圖牛逼的地方。

正文

Redis官方文檔對於位圖的介紹如下:

位圖不是一個真實的數據類型,而是定義在字符串類型上的面向 位 的操作集合。由於字符串類型是二進制安全的二進制大對象,並且最大長度是 512MB,適合於設置 2^32個不同的位。


位操作分為兩組:常量時間單個位的操作,像設置一個位為 1 或者 0,或者獲取該位的值。對一組位的操作,例如:在 給定的位 範圍內計算 設置位 的數目。


位圖的最大優勢是:是一種非常顯著的節省空間來存儲信息的方式。例如,在一個系統中,不同用戶由遞增的用戶 ID 來表示,可以使用 512MB 的內存來表示 40億 用戶的單個位信息(例如他們是否需要接收信件)。

簡而言之,位圖 是操作 比特位

的,其優點是節省 內存空間。

位圖的常用操作如下:
  • setbit 設置特定key對應的比特位的值。
  • getbit 獲取特定key對應的比特位的值。
  • bitcount統計給定key對應的字符串比特位為1的數量。

Redis 的位數組是 自動擴充,如果設置了某個偏移位置超出了現有的內容範圍,就會 自動 將位數組進行零擴充。

接下來我們使用 redis-cli 設置第一個字符,也就是 位數組 的前 8 位,我們只需要設置值為 1 的位,如 h (二進制:0 1 1 0 1 0 0 0) 字符只有 1/2/4 位需要設置。

<code>

127.0

.0

.1

:6379>

 

setbit

 

s

 

1

 

1

(integer)

 

0

127.0

.0

.1

:6379>

 

setbit

 

s

 

2

 

1

(integer)

 

0

127.0

.0

.1

:6379>

 

setbit

 

s

 

4

 

1

(integer)

 

0

127.0

.0

.1

:6379>

 

get

 

s

"h"

/<code>

上面這個例子可以理解為「零存整取」,同樣我們還也可以「零存零取」,「整存零取」。「零存」就是使用 setbit 對位值進行逐個設置,「整存」就是使用字符串一次性填充所有位數組,覆蓋掉舊值。

零存零取

<code>

127.0

.0

.1

:6379>

 

setbit

 

a

 

1

 

1

(integer)

 

0

127.0

.0

.1

:6379>

 

setbit

 

a

 

2

 

1

(integer)

 

0

127.0

.0

.1

:6379>

 

setbit

 

a

 

4

 

1

(integer)

 

0

127.0

.0

.1

:6379>

getbit

 

a

 

1

  

//獲取某個具體位置的值

 

0

/1

(integer)

 

1

/<code>

整存零取

<code>

127.0

.0

.1

:6379>

 

set

 

b

 

h

OK

127.0

.0

.1

:6379>

 

getbit

 

b

 

1

(integer)

 

1

127.0

.0

.1

:6379>

 

getbit

 

b

 

2

(integer)

 

1

127.0

.0

.1

:6379>

 

getbit

 

b

 

3

(integer)

 

0

127.0

.0

.1

:6379>

 

getbit

 

b

 

4

(integer)

 

1

/<code>

如果對應位的字節是不可打印字符,redis-cli 會顯示該字符的 16 進制形式。

<code>

127.0

.0

.1

:6379>

 

setbit

 

x

 

0

 

1

(integer)

 

0

127.0

.0

.1

:6379>

 

setbit

 

x

 

1

 

1

(integer)

 

0

127.0

.0

.1

:6379>

 

get

 

x

"\xc0"

/<code>
統計和查找

Redis 提供了位圖統計指令 bitcount 和位圖查找指令 bitpos,bitcount 用來統計指定位置範圍內 1 的個數,bitpos 用來查找指定範圍內出現的第一個 0 或 1。

比如我們可以通過 bitcount 統計用戶一共簽到了多少天,通過 bitpos 指令查找用戶從哪一天開始第一次簽到。如果指定了範圍參數[start, end],就可以統計在某個時間範圍內用戶簽到了多少天,用戶自某天以後的哪天開始簽到。

遺憾的是, start 和 end 參數是 字節索引,也就是說指定的位範圍必須是 8 的倍數,這點比較坑,而不能任意指定。這很奇怪,不是很能理解 Antirez 為什麼要這樣設計。因為這個設計,我們無法直接計算某個月內用戶簽到了多少天,而必須要將這個月所覆蓋的字節內容全部取出來 (getrange 可以取出字符串的子串) 然後在內存裡進行統計,這個非常繁瑣。

還可以將放入位圖的offset統一乘以8(一個字節佔8比特),這樣一來就可以直接用redis的bitcount來統計對應索引範圍內的比特值為1的數量了,當然這種方案的缺點也相當明顯,就是浪費內存,因為原先只需要1比特存儲的數據,現在需要8比特存儲,所以這種方案不能很好地利用位圖索引節省存儲空間的特點。

接下來我們簡單試用一下 bitcount 指令和 bitpos 指令:

<code>

127.0

.0

.1

:6379>

 

set

 

w

 

hello

OK

127.0

.0

.1

:6379>

 

bitcount

 

w

(integer)

 

2

1127.0

.0

.1

:6379>

 

bitcount

 

w

 

0

 

0

  

//

 

第一個字符中

 

1

 

的位數

(integer)

 

3

127.0

.0

.1

:6379>

 

bitcount

 

w

 

0

 

1

  

//

 

前兩個字符中

 

1

 

的位數

(integer)

 

7

127.0

.0

.1

:6379>

 

bitpos

 

w

 

0

  

//

 

第一個

 

0

 

(integer)

 

0

127.0

.0

.1

:6379>

 

bitpos

 

w

 

1

  

//

 

第一個

 

1

 

(integer)

 

1

127.0

.0

.1

:6379>

 

bitpos

 

w

 

1

 

1

 

1

  

//

 

從第二個字符算起,第一個

 

1

 

(integer)

 

9

127.0

.0

.1

:6379>

 

bitpos

 

w

 

1

 

2

 

2

  

//

 

從第三個字符算起,第一個

 

1

 

(integer)

 

17

/<code>


點關注 不迷路

以上就是這篇的全部內容,能看到這裡的都是 人才。

如果你從本篇內容有收穫,求 點贊,求 關注,求 轉發 ,讓更多的人學習到。

如果本文有任何錯誤,請批評指教,不勝感激 !


分享到:


相關文章: