不積跬步,無以至千里;不積小流,無以成江海。
碼字不易,點贊再看。
前言
欠下的債總規要還,上篇介紹基礎數據結構中有提到 bitmap。移步至: 傳送門
本篇先從一道面試題開始,有這麼一個場景:擁有 數億 用戶的電商平臺,如何統計月活??
你用 set 記錄活躍用戶的id,沒毛病,確實也可以達到效果,但是使用這種數據結構真的妥當嗎??
針對如此寶貴的內存,能省點,不香嗎??
以 1億 用戶為例: 用戶id 用int存儲, 做個比對,引出今天的主角 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>
點關注 不迷路
以上就是這篇的全部內容,能看到這裡的都是 人才。
如果你從本篇內容有收穫,求 點贊,求 關注,求 轉發 ,讓更多的人學習到。
如果本文有任何錯誤,請批評指教,不勝感激 !