假設(shè)有個(gè)百萬簽到系統(tǒng),記錄用戶簽到記錄,簽了記錄1,沒簽記錄 0,如果我們用redis的string存儲(chǔ),一年就要存1000000*365個(gè)key,會(huì)占用大量的內(nèi)存。
為了解決這個(gè)問題,Redis 提供了位圖數(shù)據(jù)結(jié)構(gòu),這樣每天的簽到記錄只占據(jù)一個(gè)位,365 天就是 365 個(gè)位,46 個(gè)字節(jié) (一個(gè)稍長一點(diǎn)的字符串) 就可以完全容納下,這就大大節(jié)約了存儲(chǔ)空間。
bitmap 存儲(chǔ)的是連續(xù)的二進(jìn)制數(shù)字(0 和 1),通過 bitmap, 只需要一個(gè) bit 位來表示某個(gè)元素對(duì)應(yīng)的值或者狀態(tài),key 就是對(duì)應(yīng)元素本身 。我們知道 8 個(gè) bit 可以組成一個(gè) byte,所以 bitmap 本身會(huì)極大的節(jié)省儲(chǔ)存空間。
1.常用命令
byte[] bytes = "hzy".getBytes();
for (byte b : bytes) {
System.out.println(Integer.toBinaryString(b));
}
執(zhí)行結(jié)果
1101000
1111010
1111001
我們使用位圖設(shè)置一個(gè)字符串"hzy",基本命令是"setbit key [offset] [value]",我們先得到ascll碼對(duì)應(yīng)的二進(jìn)制數(shù)據(jù):01101000 01111010 01111001

## 零存整取
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 4 1
(integer) 0
127.0.0.1:6379> get test
"h"
127.0.0.1:6379> setbit test 9 1
(integer) 0
127.0.0.1:6379> setbit test 10 1
(integer) 0
127.0.0.1:6379> setbit test 11 1
(integer) 0
127.0.0.1:6379> setbit test 12 1
(integer) 0
127.0.0.1:6379> setbit test 14 1
(integer) 0
127.0.0.1:6379> get test
"hz"
127.0.0.1:6379> setbit test 17 1
(integer) 0
127.0.0.1:6379> setbit test 18 1
(integer) 0
127.0.0.1:6379> setbit test 19 1
(integer) 0
127.0.0.1:6379> setbit test 20 1
(integer) 0
127.0.0.1:6379> setbit test 23 1
(integer) 0
127.0.0.1:6379> get test
"hzy"
## 零存零取
127.0.0.1:6379> getbit test 1
(integer) 1
## 整存零取
127.0.0.1:6379> set test2 h
OK
127.0.0.1:6379> getbit test2 1
(integer) 1
2.bitcount&bitpos
我們可以通過 bitcount 統(tǒng)計(jì)用戶一共簽到了多少天。通過 bitpos 指令查找用戶從哪一天開始第一次簽到,如果指定了范圍參數(shù)[start, end],就可以統(tǒng)計(jì)在某個(gè)時(shí)間范圍內(nèi)用戶簽到了多少天。用戶自某天以后的哪天開始簽到。
但是需要注意的是 start 和 end 參數(shù)是字節(jié)索引,也就是說指定的位范圍必須是 8 的倍數(shù),而不能任意指定。因?yàn)檫@個(gè)設(shè)計(jì),我們無法直接計(jì)算某個(gè)月內(nèi)用戶簽到了多少天,而必須要將這個(gè)月所覆蓋的字節(jié)內(nèi)容全部取出來 (getrange 可以取出字符串的子串) 然后在內(nèi)存里進(jìn)行統(tǒng)計(jì),這個(gè)非常繁瑣。
bitcount命令使用,bitcount key [start] [end]
127.0.0.1:6379> set name hzy
OK
127.0.0.1:6379> bitcount name
(integer) 13
127.0.0.1:6379> bitcount name 0 0 # 第一個(gè)字符中1的個(gè)數(shù)
(integer) 3
127.0.0.1:6379> bitcount name 0 1 # 前兩個(gè)字符中1的個(gè)數(shù)
(integer) 8
bitpos命令使用,bitpos key bit [start] [end]
hzy對(duì)應(yīng)的ascll碼對(duì)應(yīng)的二進(jìn)制數(shù)據(jù):01101000 01111010 01111001
127.0.0.1:6379> bitpos name 0 #第一個(gè)0的下標(biāo)
(integer) 0
127.0.0.1:6379> bitpos name 1 #第一個(gè)1的下標(biāo)
(integer) 1
127.0.0.1:6379> bitpos name 1 2 10 #從第三個(gè)字符起,第一個(gè)1的下標(biāo)(也就是說hz不算,只有y參與)
(integer) 17
3.bitfield
我們?cè)O(shè)置 (setbit) 和獲取 (getbit) 指定位的值都是單個(gè)位的,如果要一次操作多個(gè)位,就必須使用管道來處理。不過 Redis 的 3.2 版本提供了bitfield指令。
bitfield key [GET type offset] [SET type offset value] [INCRBY type offset increment] [OVERFLOW WRAP|SAT|FAIL]
## 模擬用戶2021年7月15日簽到,偏移量從0開始
127.0.0.1:6379> setbit userid:sign:202107 14 1
(integer) 0
## 模擬用戶2021年7月16日簽到,偏移量從0開始
127.0.0.1:6379> setbit userid:sign:202107 15 1
(integer) 0
## 模擬用戶2021年7月31日簽到,偏移量從0開始
127.0.0.1:6379> setbit userid:sign:202107 30 1
(integer) 0
## 獲取用戶2021年7月的簽到數(shù)據(jù)
127.0.0.1:6379> bitfield userid:sign:202107 get u31 0
1) (integer) 98305
// 這是偽代碼,key是我寫死的,真實(shí)場(chǎng)景下,userid是用戶的ID
public static void main(String[] args) {
LocalDate date = LocalDate.now();
Map<String, Boolean> signMap = new TreeMap<>();
List<Long> list = jedis.bitfield("userid:sign:202107", "GET", String.format("u%d", date.lengthOfMonth()), "0");
if (list != null && list.size() > 0) {
// 由低位到高位,為0表示未簽,為1表示已簽
long v = list.get(0) == null ? 0 : list.get(0);
for (int i = date.lengthOfMonth(); i > 0; i--) {
LocalDate d = date.withDayOfMonth(i);
signMap.put(formatDate(d, "yyyy-MM-dd"), v >> 1 << 1 != v);
v >>= 1;
}
}
System.out.println(signMap.toString());
}
private static String formatDate(LocalDate date, String pattern) {
return date.format(DateTimeFormatter.ofPattern(pattern));
}
// 執(zhí)行結(jié)果
{2021-07-01=false, 2021-07-02=false, 2021-07-03=false,
2021-07-04=false, 2021-07-05=false, 2021-07-06=false,
2021-07-07=false, 2021-07-08=false, 2021-07-09=false,
2021-07-10=false, 2021-07-11=false, 2021-07-12=false,
2021-07-13=false, 2021-07-14=false, 2021-07-15=true,
2021-07-16=true, 2021-07-17=false, 2021-07-18=false,
2021-07-19=false, 2021-07-20=false, 2021-07-21=false,
2021-07-22=false, 2021-07-23=false, 2021-07-24=false,
2021-07-25=false, 2021-07-26=false, 2021-07-27=false,
2021-07-28=false, 2021-07-29=false, 2021-07-30=false,
2021-07-31=true}
注意:i是有符號(hào)整數(shù) u是無符號(hào)整數(shù),例如u8是一個(gè)8位的無符號(hào)整數(shù),有符號(hào)最多可以獲取64位,無符號(hào)最多可以獲取63位
我們使用bitfield key set子指令,把最后一個(gè)字符串y,改為大寫的Y,Y的ASCII碼是89
127.0.0.1:6379> set name hzy
OK
127.0.0.1:6379> bitfield name set u8 16 89
1) (integer) 121
127.0.0.1:6379> get name
"hzY"