Redis-BitMap

dzzgml發表於2018-02-09
 BitMap是什麼

             通過一個bit位來表示某個元素對應的值或者狀態,其中的key就是對應元素本身。Bitmaps 本身不是一種資料結構,實際上它就是字串(key 對應的 value 就是上圖中最後的一串二進位制),但是它可以對字串的位進行操作。 Bitmaps 單獨提供了一套命令,所以在 Redis 中使用 Bitmaps 和使用字串的方法不太相同。可以把 Bitmaps 想象成一個以 位 為單位的陣列,陣列的每個單元只能儲存 0 和 1,陣列的下標在Bitmaps中叫做偏移量。


BitMap的命令

SETBIT

語法:SETBIT key offset value

說明:

key 所儲存的字串值,設定或清除指定偏移量上的位(bit)。

位的設定或清除取決於 value 引數,可以是 0 也可以是 1

key 不存在時,自動生成一個新的字串值。

字串會進行伸展(grown)以確保它可以將 value 儲存在指定的偏移量上。當字串值進行伸展時,空白位置以 0 填充。

offset 引數必須大於或等於 0 ,小於 2^32 (bit 對映被限制在 512 MB 之內)。

對使用大的 offsetSETBIT 操作來說,記憶體分配可能造成 Redis 伺服器被阻塞。

返回值:

字串值指定偏移量上原來儲存的位(bit)。

示例:
# SETBIT 會返回之前位的值(預設是 0)這裡會生成 126 個位
coderknock> SETBIT testBit 125 1
(integer) 0
coderknock> SETBIT testBit 125 0
(integer) 1
coderknock> SETBIT testBit 125 1
(integer) 0
coderknock> GETBIT testBit 125
(integer) 1
coderknock> GETBIT testBit 100
(integer) 0
# SETBIT  value 只能是 0 或者 1  二進位制只能是0或者1
coderknock> SETBIT testBit 618 2
(error) ERR bit is not an integer or out of range複製程式碼

獲取值

GETBIT


語法:GETBIT key offset
說明:

key 所儲存的字串值,獲取指定偏移量上的位(bit)。

offset 比字串值的長度大,或者 key 不存在時,返回 0

返回值:

字串值指定偏移量上的位(bit)。

示例:
coderknock> EXISTS bit
(integer) 0
coderknock> SETBIT bit 125 1
(integer) 0
coderknock> GETBIT bit 125
(integer) 1
# 偏移量如果不存在則是0
coderknock> GETBIT bit 126
(integer) 0
# 可以看到 bit 本身也是個字串
coderknock> GET bit
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00 "複製程式碼

獲取Bitmaps 指定範圍值為 1 的位個數

BITCOUNT


語法:BITCOUNT key start
說明:

計算給定字串中,被設定為 1 的位元位的數量。

一般情況下,給定的整個字串都會被進行計數,通過指定額外的 startend 引數,可以讓計數只在特定的位上進行。

startend 引數的設定和 GETRANGE 命令類似,都可以使用負數值: 比如 -1 表示最後一個位元組, -2表示倒數第二個位元組,以此類推。

不存在的 key 被當成是空字串來處理,因此對一個不存在的 key 進行 BITCOUNT 操作,結果為 0

返回值:

被設定為 1 的位的數量。

示例:
# 此處的 bit 基於 GETBIT 示例的命令中的
coderknock> BITCOUNT bit
(integer) 1
# 計算 bit 中所有值為 1 的位的個數
coderknock> BITCOUNT bit
(integer) 1
coderknock> SETBIT bit 0 1
(integer) 0
coderknock> BITCOUNT bit
(integer) 2
# 計算指定位置 bit 中所有值為 1 的位的個數
coderknock> BITCOUNT bit 10 126
(integer) 1複製程式碼

多個 Bitmaps 運算

BITOP


語法:BITOP operation destkey key [key ...]
說明:

對一個或多個儲存二進位制位的字串 key 進行位元操作,並將結果儲存到 destkey 上。

operation 可以是 ANDORNOTXOR 這四種操作中的任意一種:

  • 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 作為輸入。

處理不同長度的字串

BITOP 處理不同長度的字串時,較短的那個字串所缺少的部分會被看作 0

空的 key 也被看作是包含 0 的字串序列。

返回值:

儲存到 destkey 的字串的長度,和輸入 key 中最長的字串長度相等。

示例:
coderknock> SETBIT bits-1 0 1
(integer) 0
coderknock> SETBIT bits-1 3 1
(integer) 0
# bits-1 為 1001

coderknock> SETBIT bits-2 0 1
(integer) 0
coderknock> SETBIT bits-2 1 1
(integer) 0
coderknock> SETBIT bits-2 3 1
(integer) 0
# bits-2 為 1011

#bits-1 bits-2 做 並 操作
coderknock> BITOP AND and-result bits-1 bits-2
(integer) 1
coderknock> GETBIT and-result 0
(integer) 1
coderknock> GETBIT and-result 1
(integer) 0
coderknock> GETBIT and-result 2
(integer) 0
coderknock> GETBIT and-result 3
(integer) 1
#and-result 1001

#bits-1 bits-2 做 或 操作
coderknock> BITOP OR or-result bits-1 bits-2
(integer) 1
coderknock> GETBIT and-result 0
(integer) 1
coderknock> GETBIT and-result 1
(integer) 0
coderknock> GETBIT and-result 2
(integer) 0
coderknock> GETBIT and-result 3
(integer) 1
#or-result 1011

# 非 操作只能針對一個 key
coderknock> BITOP NOT not-result bits-1 bits-2
(error) ERR BITOP NOT must be called with a single source key.
coderknock> BITOP NOT not-result bits-1
(integer) 1
coderknock> GETBIT not-result 0
(integer) 0
coderknock> GETBIT not-result 1
(integer) 1
coderknock> GETBIT not-result 2
(integer) 1
coderknock> GETBIT not-result 3
(integer) 0
# not-result 0110

# 異或操作
coderknock> BITOP XOR xor-result bits-1 bits-2
(integer) 1
coderknock> GETBIT xor-result 0
(integer) 0
coderknock> GETBIT xor-result 1
(integer) 1
coderknock> GETBIT xor-result 2
(integer) 0
coderknock> GETBIT xor-result 3
(integer) 0
# xor-result 0010複製程式碼

BITOP 的複雜度為 O(N) ,當處理大型矩陣(matrix)或者進行大資料量的統計時,最好將任務指派到附屬節點(slave)進行,避免阻塞主節點。

計算Bitmaps中第一個值為 bit 的偏移量

BITPOS

自2.8.7可用。

時間複雜度:O(N)。

語法:BITPOS key bit [start][end]
說明:

返回字串裡面第一個被設定為 1 或者 0 的bit位。

返回一個位置,把字串當做一個從左到右的位元組陣列,第一個符合條件的在位置 0,其次在位置 8,等等。

GETBITSETBIT 相似的也是操作位元組位的命令。

預設情況下整個字串都會被檢索一次,只有在指定 start 和 end 引數(指定start和end位是可行的),該範圍被解釋為一個位元組的範圍,而不是一系列的位。所以start=0 並且 end=2是指前三個位元組範圍內查詢。

注意,返回的位的位置始終是從 0 開始的,即使使用了 start 來指定了一個開始位元組也是這樣。

GETRANGE 命令一樣,start 和 end 也可以包含負值,負值將從字串的末尾開始計算,-1是字串的最後一個位元組,-2是倒數第二個,等等。

不存在的key將會被當做空字串來處理。

返回值:

命令返回字串裡面第一個被設定為 1 或者 0 的 bit 位。

如果我們在空字串或者 0 位元組的字串裡面查詢 bit 為1的內容,那麼結果將返回-1。

如果我們在字串裡面查詢 bit 為 0 而且字串只包含1的值時,將返回字串最右邊的第一個空位。如果有一個字串是三個位元組的值為 0xff 的字串,那麼命令 BITPOS key 0 將會返回 24,因為 0-23 位都是1。

基本上,我們可以把字串看成右邊有無數個 0。

然而,如果你用指定 start 和 end 範圍進行查詢指定值時,如果該範圍內沒有對應值,結果將返回 -1。

示例:
redis> SET mykey "\xff\xf0\x00"
OK
redis> BITPOS mykey 0 # 查詢字串裡面bit值為0的位置
(integer) 12
redis> SET mykey "\x00\xff\xf0"
OK
redis> BITPOS mykey 1 0 # 查詢字串裡面bit值為1從第0個位元組開始的位置
(integer) 8
redis> BITPOS mykey 1 2 # 查詢字串裡面bit值為1從第2個位元組(12)開始的位置
(integer) 16
redis> set mykey "\x00\x00\x00"
OK
redis> BITPOS mykey 1 # 查詢字串裡面bit值為1的位置
                    (integer) -1複製程式碼

BITFIELD

自3.2.0可用。

時間複雜度:每個子命令的複雜度為 O(1) 。

語法:BITFIELD key [GET type offset][SET type offset value][INCRBY type offset increment][OVERFLOW WRAP|SAT|FAIL]
說明:

BITFIELD key GET type offset INCRBY type offset increment

                                                                                  `BITFIELD` 命令可以將一個 Redis 字串看作是一個由二進位制位組成的陣列, 並對這個陣列中儲存的長度不同的整數進行訪問 (被儲存的整數無需進行對齊)。 換句話說, 通過這個命令, 使用者可以執行諸如 “對偏移量 1234 上的 5 位長有符號整數進行設定”、 “獲取偏移量 4567 上的 31 位長無符號整數”等操作。 此外, `BITFIELD` 命令還可以對指定的整數執行加法操作和減法操作, 並且這些操作可以通過設定妥善地處理計算時出現的溢位情況。
複製程式碼

BITFIELD 命令可以在一次呼叫中同時對多個位範圍進行操作: 它接受一系列待執行的操作作為引數, 並返回一個陣列作為回覆, 陣列中的每個元素就是對應操作的執行結果。

比如以下命令就展示瞭如何對位於偏移量 100 的 8 位長有符號整數執行加法操作, 並獲取位於偏移量 0 上的 4 位長無符號整數:

coderknock> BITFIELD mykey INCRBY i8 100 1 GET u4 0
1) (integer) 1
2) (integer) 0複製程式碼

注意:

  • 使用 GET 子命令對超出字串當前範圍的二進位制位進行訪問(包括鍵不存在的情況), 超出部分的二進位制位的值將被當做是 0 。

  • 使用 SET 子命令或者 INCRBY 子命令對超出字串當前範圍的二進位制位進行訪問將導致字串被擴大, 被擴大的部分會使用值為 0 的二進位制位進行填充。 在對字串進行擴充套件時, 命令會根據字串目前已有的最遠端二進位制位, 計算出執行操作所需的最小長度。

支援的子命令以及數字型別

以下是 BITFIELD 命令支援的子命令:

  • GET <type> <offset> —— 返回指定的二進位制位範圍。

  • SET <type> <offset> <value> —— 對指定的二進位制位範圍進行設定,並返回它的舊值。

  • INCRBY <type> <offset> <increment> —— 對指定的二進位制位範圍執行加法操作,並返回它的舊值。使用者可以通過向 increment 引數傳入負值來實現相應的減法操作。

除了以上三個子命令之外, 還有一個子命令, 它可以改變之後執行的 INCRBY 子命令在發生溢位情況時的行為:

  • OVERFLOW [WRAP|SAT|FAIL]

當被設定的二進位制位範圍值為整數時, 使用者可以在型別引數的前面新增 i 來表示有符號整數, 或者使用 u 來表示無符號整數。 比如說, 我們可以使用 u8 來表示 8 位長的無符號整數, 也可以使用 i16 來表示 16 位長的有符號整數。

BITFIELD 命令最大支援 64 位長的有符號整數以及 63 位長的無符號整數, 其中無符號整數的 63 位長度限制是由於 Redis 協議目前還無法返回 64 位長的無符號整數而導致的。

二進位制位和位置偏移量

在二進位制位範圍命令中, 使用者有兩種方法來設定偏移量:

  • 如果使用者給定的是一個沒有任何字首的數字, 那麼這個數字指示的就是字串以零為開始(zero-base)的偏移量。

  • 另一方面, 如果使用者給定的是一個帶有 # 字首的偏移量, 那麼命令將使用這個偏移量與被設定的數字型別的位長度相乘, 從而計算出真正的偏移量。

比如說, 對於以下這個命令來說:

BITFIELD mystring SET i8 #0 100 i8 #1 200
複製程式碼

命令會把 mystring 鍵裡面, 第一個 i8 長度的二進位制位的值設定為 100 , 並把第二個 i8 長度的二進位制位的值設定為 200 。 當我們把一個字串鍵當成陣列來使用, 並且陣列中儲存的都是同等長度的整數時, 使用 # 字首可以讓我們免去手動計算被設定二進位制位所在位置的麻煩。

溢位控制

使用者可以通過 OVERFLOW 命令以及以下展示的三個引數, 指定 BITFIELD 命令在執行自增或者自減操作時, 碰上向上溢位(overflow)或者向下溢位(underflow)情況時的行為:

  • WRAP : 使用迴繞(wrap around)方法處理有符號整數和無符號整數的溢位情況。 對於無符號整數來說, 迴繞就像使用數值本身與能夠被儲存的最大無符號整數執行取模計算, 這也是 C 語言的標準行為。 對於有符號整數來說, 上溢將導致數字重新從最小的負數開始計算, 而下溢將導致數字重新從最大的正數開始計算。 比如說, 如果我們對一個值為 127i8 整數執行加一操作, 那麼將得到結果 -128

  • SAT : 使用飽和計算(saturation arithmetic)方法處理溢位, 也即是說, 下溢計算的結果為最小的整數值, 而上溢計算的結果為最大的整數值。 舉個例子, 如果我們對一個值為 120i8 整數執行加 10計算, 那麼命令的結果將為 i8 型別所能儲存的最大整數值 127 。 與此相反, 如果一個針對 i8 值的計算造成了下溢, 那麼這個 i8 值將被設定為 -127

  • FAIL : 在這一模式下, 命令將拒絕執行那些會導致上溢或者下溢情況出現的計算, 並向使用者返回空值表示計算未被執行。

需要注意的是, OVERFLOW 子命令只會對緊隨著它之後被執行的 INCRBY 命令產生效果, 這一效果將一直持續到與它一同被執行的下一個 OVERFLOW 命令為止。 在預設情況下, INCRBY 命令使用 WRAP 方式來處理溢位計算。

以下是一個使用 OVERFLOW 子命令來控制溢位行為的例子:

coderknock> BITFIELD mykey incrby u2 100 1 OVERFLOW SAT incrby u2 102 1
1) (integer) 1
2) (integer) 1

coderknock> BITFIELD mykey incrby u2 100 1 OVERFLOW SAT incrby u2 102 1
1) (integer) 2
2) (integer) 2

coderknock> BITFIELD mykey incrby u2 100 1 OVERFLOW SAT incrby u2 102 1
1) (integer) 3
2) (integer) 3

coderknock> BITFIELD mykey incrby u2 100 1 OVERFLOW SAT incrby u2 102 1
1) (integer) 0  -- 使用預設的 WRAP 方式處理溢位
2) (integer) 3  -- 使用 SAT 方式處理溢位複製程式碼

而以下則是一個因為 OVERFLOW FAIL 行為而導致子命令返回空值的例子:

coderknock> BITFIELD mykey OVERFLOW FAIL incrby u2 102 1
1) (nil)複製程式碼
作用

BITFIELD 命令的作用在於它能夠將很多小的整數儲存到一個長度較大的點陣圖中, 又或者將一個非常龐大的鍵分割為多個較小的鍵來進行儲存, 從而非常高效地使用記憶體, 使得 Redis 能夠得到更多不同的應用 —— 特別是在實時分析領域: BITFIELD 能夠以指定的方式對計算溢位進行控制的能力, 使得它可以被應用於這一領域。

效能注意事項

BITFIELD 在一般情況下都是一個快速的命令, 需要注意的是, 訪問一個長度較短的字串的遠端二進位制位將引發一次記憶體分配操作, 這一操作花費的時間可能會比命令訪問已有的字串花費的時間要長。

二進位制位的排列

BITFIELD 把點陣圖第一個位元組偏移量 0 上的二進位制位看作是 most significant 位, 以此類推。 舉個例子, 如果我們對一個已經預先被全部設定為 0 的點陣圖進行設定, 將它在偏移量 7 的值設定為 5 位無符號整數值 23 (二進位制位為 10111 ), 那麼命令將生產出以下這個點陣圖表示:

+--------+--------+
|00000001|01110000|
+--------+--------+
複製程式碼

當偏移量和整數長度與位元組邊界進行對齊時, BITFIELD 表示二進位制位的方式跟大端表示法(big endian)一致, 但是在沒有對齊的情況下, 理解這些二進位制位是如何進行排列也是非常重要的。

返回值:

如果 member 元素是集合的成員,返回 1

如果 member 元素不是集合的成員,或 key 不存在,返回 0

示例:
coderknock> SISMEMBER saddTest add1
(integer) 1
#  add7  元素不存在
coderknock> SISMEMBER saddTest add7
(integer) 0
# key 不存在
coderknock> SISMEMBER nonSet a
(integer) 0
# key 型別不是集合
coderknock> SISMEMBER embstrKey a
(error) WRONGTYPE Operation against a key holding the wrong kind of value    複製程式碼

案例

使用場景一:使用者簽到

Jedis redis = new Jedis("192.168.31.89",6379,100000);
//使用者uid
String uid = "1";
String cacheKey = "sign_"+Integer.valueOf(uid);
//記錄有uid的key
// $cacheKey = sprintf("sign_%d", $uid);

//開始有簽到功能的日期
String startDate = "2017-01-01";

//今天的日期
String todayDate = "2017-01-21";

//計算offset(時間搓)
long startTime = dateParase(startDate,"yyyy-MM-dd").getTime();
long todayTime = dateParase(todayDate,"yyyy-MM-dd").getTime();
long offset = (long) Math.floor((todayTime - startTime) / 86400);

System.out.println("今天是第"+offset+"天");

//簽到
//一年一個使用者會佔用多少空間呢?大約365/8=45.625個位元組,好小,有木有被驚呆?
redis.setbit(cacheKey,offset,"1");

//查詢簽到情況
boolean bitStatus = redis.getbit(cacheKey, offset);
//判斷是否已經簽到
//計算總簽到次數
long qdCount = redis.bitcount(cacheKey);複製程式碼

使用場景二:統計活躍使用者

 使用時間作為cacheKey,然後使用者ID為offset,如果當日活躍過就設定為1 那麼我該如果計算某幾天/月/年的活躍使用者呢(暫且約定,統計時間內只有有一天線上就稱為活躍),有請下一個redis的命令 命令 BITOP operation destkey key [key ...] 說明:對一個或多個儲存二進位制位的字串 key 進行位元操作,並將結果儲存到 destkey 上。 說明:BITOP 命令支援 AND 、 OR 、 NOT 、 XOR 這四種操作中的任意一種引數 

Map<String,List<Integer>>dateActiveuser = new HashMap<>();
Jedis redis = new Jedis("192.168.31.89",6379,100000);
Integer[] temp01 = {1,2,3,4,5,6,7,8,9,10};
List<Integer>temp01List = new ArrayList<>();
Collections.addAll(temp01List,temp01);
dateActiveuser.put("2017-01-10",temp01List);


Integer[] temp02 = {1,2,3,4,5,6,7,8};
List<Integer>temp02List = new ArrayList<>();
Collections.addAll(temp02List,temp02);
dateActiveuser.put("2017-01-11",temp02List);

Integer[] temp03 = {1,2,3,4,5,6};
List<Integer>temp03List = new ArrayList<>();
Collections.addAll(temp03List,temp03);
dateActiveuser.put("2017-01-12",temp03List);

Integer[] temp04 = {1,4,5,6};
List<Integer>temp04List = new ArrayList<>();
Collections.addAll(temp04List,temp04);
dateActiveuser.put("2017-01-13",temp04List);

Integer[] temp05 = {1,4,5,6};
List<Integer>temp05List = new ArrayList<>();
Collections.addAll(temp05List,temp05);
dateActiveuser.put("2017-01-14",temp05List);

String date[] = {"2017-01-10","2017-01-11","2017-01-12","2017-01-13","2017-01-14"};

//測試資料放入redis中
for (int i=0;i<date.length;i++){
    for (int j=0;j<dateActiveuser.get(date[i]).size();j++){
        redis.setbit(date[i], dateActiveuser.get(date[i]).get(j), "1");
    }
}

//bitOp
redis.bitop(BitOP.AND, "stat", "stat_2017-01-10", "stat_2017-01-11","stat_2017-01-12");

System.out.println("總活躍使用者:"+redis.bitcount("stat"));

redis.bitop(BitOP.AND, "stat1", "stat_2017-01-10", "stat_2017-01-11","stat_2017-01-14");
System.out.println("總活躍使用者:"+redis.bitcount("stat1"));

redis.bitop(BitOP.AND, "stat2", "stat_2017-01-10", "stat_2017-01-11");
System.out.println("總活躍使用者:"+redis.bitcount("stat2"));複製程式碼