redis 4 點陣圖Bitmaps
一般知道前面介紹的五種redis資料結構,就可以開心的玩耍了,但如果知道Bitmaps,Hyperloglogs,GEO,就更開心了。
這次我們來看下Bitmaps。
簡介
假設一個場景:記錄使用者的簽到天數。
方法一:將使用者的id和日期關聯起來,做個key,比如使用者007在2018/08/14這天的簽到情況,設定個key:sign_2018_08_14_007,並將值設定為1。如果使用者越來越多,數值則越來越大,到後面的統計耗時肯定越來越長,並且儲存量也越來越大。
方法二:用hash結構,uid做key,裡面存簽到的日期,但儲存量還是很大。
有沒有更優雅的方法呢?有,答案是bitmaps
。
在看bitmaps的概念前,先做個小驗證:
1. 設定個key的值為h
127.0.0.1:6379> set w h
OK
127.0.0.1:6379> get w
"h"
h對應的ASCII為0110 1000
,即:
2.通過getbit來驗證下
127.0.0.1:6379> getbit w 0 #用getbit獲取w第0位的值
(integer) 0
127.0.0.1:6379> getbit w 1 #用getbit獲取w第1位的值
(integer) 1
127.0.0.1:6379> getbit w 2 #用getbit獲取w第2位的值
(integer) 1
127.0.0.1:6379> getbit w 3
(integer ) 0
127.0.0.1:6379> getbit w 4
(integer) 1
127.0.0.1:6379> getbit w 5
(integer) 0
127.0.0.1:6379> getbit w 6
(integer) 0
127.0.0.1:6379> getbit w 7
(integer) 0
到這裡,對bitmaps也就有了個大概的感受了。
bitmaps不是特殊的資料結構,它的內容其實就是普通的字串,也就是 byte 陣列。我們可以使用普通的 get/set 直接獲取和設定整個點陣圖的內容,也可以使用點陣圖操作 getbit/setbit 等將 byte 陣列看成「位陣列」來處理。
用字串’world’來做個例子,首先看下world對應的ASCII值,可以用python直接獲取
>>> bin(ord('w'))
'0b1110111'
>>> bin(ord('o'))
'0b1101111'
>>> bin(ord('r'))
'0b1110010'
>>> bin(ord('l'))
'0b1101100'
>>> bin(ord('d'))
'0b1100100'
用setbit來設定個key,值為’w’
127.0.0.1:6379> get test
(nil)
127.0.0.1:6379> setbit test 0 0
(integer) 0
127.0.0.1:6379> setbit test 1 1
(integer) 0
127.0.0.1:6379> setbit test 2 1
(integer) 0
127.0.0.1:6379> setbit test 3 1
(integer) 0
127.0.0.1:6379> setbit test 4 0
(integer) 0
127.0.0.1:6379> setbit test 5 1
(integer) 0
127.0.0.1:6379> setbit test 6 1
(integer) 0
127.0.0.1:6379> setbit test 7 1
(integer) 0
127.0.0.1:6379> get test
"w"
把’world’對應的值從左到右放在一起
01110111 01101111 01110010 01101100 01100100
即
0111011101101111011100100110110001100100
redis驗證下
127.0.0.1:6379> set t world
OK
127.0.0.1:6379> getbit t 0
(integer) 0
127.0.0.1:6379> getbit t 2
(integer) 1
127.0.0.1:6379> getbit t 7
(integer) 1
127.0.0.1:6379> getbit t 14
(integer) 1
統計和查詢
Redis 提供了點陣圖統計指令 bitcount 和點陣圖查詢指令 bitpos,bitcount 用來統計指定位置範圍內 1 的個數,bitpos 用來查詢指定範圍內出現的第一個 0 或 1。
比如我們可以通過 bitcount 統計使用者一共簽到了多少天,通過 bitpos 指令查詢使用者從哪一天開始第一次簽到。
bitcount
格式
BITCOUNT key [start] [end]
統計下’world’有多少個1:
127.0.0.1:6379> bitcount t
(integer) 23
統計下’wo’有多少個1:
127.0.0.1:6379> bitcount t 0 1 #wo是前兩個字元,所以範圍是0-1
(integer) 12
統計下’orl’有多少個1:
127.0.0.1:6379> bitcount t 1 3
(integer) 14
注意點:
start 和 end 引數是位元組索引,也就是說指定的位範圍必須是 8 的倍數,而不能任意指定。
bitpos
顯示0首次出現的位置
127.0.0.1:6379> bitpos t 0
(integer) 0
顯示1首次出現的位置
127.0.0.1:6379> bitpos t 1
(integer) 1
bitfield
前文我們設定 (setbit) 和獲取 (getbit) 指定位的值都是單個位的,如果要一次操作多個位,就必須使用管道來處理。
Redis 的 3.2 版本以後新增了一個指令bitfield,通過這個指令可以一次進行多個位操作。
bitfield 有三個子指令,分別是 get/set/incrby,它們都可以對指定位片段進行讀寫,但是最多隻能處理 64 個連續的位,如果超過 64 位,就得使用多個子指令,bitfield 可以一次執行多個子指令。
回到我們剛才設定的’world’,它對應的二進位制碼如下:
0111011101101111011100100110110001100100
get
這次我們使用bitfield的get來獲取它的有符號數和無符號數
127.0.0.1:6379> get t
"world"
127.0.0.1:6379> bitfield t get i4 0 #i表示有符號位,4表示連續取4位,0表示開始位置
1) (integer) 7
我們算下,看值是不是7.
從0位開始,取4位,即0111。首位是0,所以直接將111轉為十進位制,得到值7,與結果一樣
我們再算個:
127.0.0.1:6379> bitfield t get i3 7
1) (integer) -3
看下是不是-3
從7位開始,取3位,即101。首位是1,是負數,後面01為補碼,先減1,再反轉,得到11,轉為十進位制,即3,得到值-3,與結果一直
再驗證個有符號位:
127.0.0.1:6379> bitfield t get u3 7
1) (integer) 5
同樣從7位開始,取3位,即101。由於是無符號位,所以直接求值,值為5,與結果一致
多個同時操作
127.0.0.1:6379> bitfield t get u3 7 get i3 7 get i4 1
1) (integer) 5
2) (integer) -3
3) (integer) -2
set
我們用bitfield的set指令在’world’後增加個’w’。
‘w’在ASCII中對應的值119,加在’world’後,屬於第六個字母,那麼在8位的二進位制中則是從40位開始,所以指令如下:
127.0.0.1:6379> bitfield t set u8 40 119 #設定無符號位,連續8位,從40位開始,值為119
1) (integer) 0
127.0.0.1:6379> get t
"worldw"
使用同樣的計算方式,再增加三個字母’ild’
127.0.0.1:6379> bitfield t set u8 48 105 set u8 56 108 set u8 64 100
1) (integer) 0
2) (integer) 0
3) (integer) 0
127.0.0.1:6379> get t
"worldwild"
我們得到了一個新的單詞’worldwild’
incrby
再看第三個子指令 incrby,它用來對指定範圍的位進行自增操作。既然提到自增,就有可能出現溢位。如果增加了正數,會出現上溢,如果增加的是負數,就會出現下溢位。Redis 預設的處理是折返。如果出現了溢位,就將溢位的符號位丟掉。如果是 8 位無符號數 255,加 1 後就會溢位,會全部變零。如果是 8 位有符號數 127,加 1 後就會溢位變成 -128。
做下實驗
127.0.0.1:6379> set a w
OK
127.0.0.1:6379> getbit a 0
(integer) 0
127.0.0.1:6379> getbit a 1
(integer) 1
127.0.0.1:6379> getbit a 2
(integer) 1
127.0.0.1:6379> getbit a 3
(integer) 1
從0位開始,對連續的4位無符號數做自增
127.0.0.1:6379> bitfield a incrby u4 0 1
1) (integer) 8
看下結果
127.0.0.1:6379> get a
"\x87"
127.0.0.1:6379> getbit a 0
(integer) 1
127.0.0.1:6379> getbit a 1
(integer) 0
127.0.0.1:6379> getbit a 2
(integer) 0
127.0.0.1:6379> getbit a 3
(integer) 0
測試下溢位
127.0.0.1:6379> setbit a 0 1
(integer) 0
127.0.0.1:6379> setbit a 1 1
(integer) 1
127.0.0.1:6379> setbit a 2 1
(integer) 1
127.0.0.1:6379> setbit a 3 1
(integer) 1
127.0.0.1:6379> setbit a 4 1
(integer) 0
127.0.0.1:6379> setbit a 5 1
(integer) 1
127.0.0.1:6379> setbit a 6 1
(integer) 1
127.0.0.1:6379> setbit a 7 1
(integer) 1
127.0.0.1:6379> get a
"\xff"
127.0.0.1:6379> bitfield a incrby u8 0 1
1) (integer) 0
127.0.0.1:6379> get a
"\x00"
127.0.0.1:6379> getbit a 0
(integer) 0
127.0.0.1:6379> getbit a 1
(integer) 0
127.0.0.1:6379> getbit a 2
(integer) 0
127.0.0.1:6379>
overflow
bitfield 指令提供了溢位策略子指令 overflow,使用者可以選擇溢位行為,預設是折返 (wrap),還可以選擇失敗 (fail) 報錯不執行,以及飽和截斷 (sat),超過了範圍就停留在最大最小值。overflow 指令隻影響接下來的第一條指令,這條指令執行完後溢位策略會變成預設值折返 (wrap)。
折返 (wrap)
127.0.0.1:6379> setbit a 0 1
(integer) 0
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 3 1
(integer) 0
127.0.0.1:6379> setbit a 4 1
(integer) 0
127.0.0.1:6379> setbit a 5 1
(integer) 0
127.0.0.1:6379> setbit a 6 1
(integer) 0
127.0.0.1:6379> setbit a 7 1
(integer) 0
127.0.0.1:6379> bitfield a overflow wrap incrby u8 0 1
1) (integer) 0
127.0.0.1:6379> get a
"\x00"
失敗 (fail)
127.0.0.1:6379> setbit a 0 1
(integer) 0
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 3 1
(integer) 0
127.0.0.1:6379> setbit a 4 1
(integer) 0
127.0.0.1:6379> setbit a 5 1
(integer) 0
127.0.0.1:6379> setbit a 6 1
(integer) 0
127.0.0.1:6379> setbit a 7 1
(integer) 0
127.0.0.1:6379> get a
"\xff"
127.0.0.1:6379> bitfield a overflow fail incrby u8 0 1
1) (nil)
截斷 (sat)
127.0.0.1:6379> bitfield a overflow sat incrby u8 0 1
1) (integer) 255
127.0.0.1:6379> get a
"\xff"