1. 數(shù)據(jù)結(jié)構(gòu)模型
現(xiàn)代計算機用二進制(位)作為信息的基礎(chǔ)單位,1個字節(jié)等于8位,例如“big”字符串是由3個字節(jié)組成,但實際在計算機存儲時將其用二進制表示,“big”分別對應(yīng)的ASCII碼分別是98、105、103,對應(yīng)的二進制分別是01100010、01101001和01100111,如圖所示。

許多開發(fā)語言都提供了操作位的功能,合理地使用位能夠有效地提高內(nèi)存使用率和開發(fā)效率。Redis提供了Bitmaps這個“數(shù)據(jù)結(jié)構(gòu)”可以實現(xiàn)對位的操作。把數(shù)據(jù)結(jié)構(gòu)加上引號主要因為:
- Bitmaps本身不是一種數(shù)據(jù)結(jié)構(gòu),實際上它就是字符串,但是它可以對字符串的位進行操作。
-
Bitmaps單獨提供了一套命令,所以在Redis中使用Bitmaps和使用字符串的方法不太相同??梢园袯itmaps想象成一個以位為單位的數(shù)組,數(shù)組的每個單元只能存儲0和1,數(shù)組的下標(biāo)在Bitmaps中叫做偏移量。
字符串"big"用二進制表示
2. 命令
本節(jié)將每個獨立用戶是否訪問過網(wǎng)站存放在Bitmaps中,將訪問的用戶記做1,沒有訪問的用戶記做0,用偏移量作為用戶的id。
1)設(shè)置值
setbit key offset value
設(shè)置鍵的第offset個位的值(從0算起),假設(shè)現(xiàn)在有20個用戶,userid=0,5,11,15,19的用戶對網(wǎng)站進行了訪問,那么當(dāng)前Bitmaps初始化結(jié)果如圖所示。

具體操作過程如下,unique:users:2016-04-05代表2016-04-05這天的獨立訪問用戶的Bitmaps:
127.0.0.1:6379> setbit unique:users:2016-04-05 0 1
(integer) 0
127.0.0.1:6379> setbit unique:users:2016-04-05 5 1
(integer) 0
127.0.0.1:6379> setbit unique:users:2016-04-05 11 1
(integer) 0
127.0.0.1:6379> setbit unique:users:2016-04-05 15 1
(integer) 0
127.0.0.1:6379> setbit unique:users:2016-04-05 19 1
(integer) 0
如果此時有一個userid=50的用戶訪問了網(wǎng)站,那么Bitmaps的結(jié)構(gòu)變成了如圖所示,第20位~49位都是0。

很多應(yīng)用的用戶id以一個指定數(shù)字(例如10000)開頭,直接將用戶id和Bitmaps的偏移量對應(yīng)勢必會造成一定的浪費,通常的做法是每次做setbit操作時將用戶id減去這個指定數(shù)字。在第一次初始化Bitmaps時,假如偏移量非常大,那么整個初始化過程執(zhí)行會比較慢,可能會造成Redis的阻塞。
2)獲取值
gitbit key offset
獲取鍵的第offset位的值(從0開始算),下面操作獲取id=8的用戶是否在2016-04-05這天訪問過,返回0說明沒有訪問過:
127.0.0.1:6379> getbit unique:users:2016-04-05 8
(integer) 0
由于offset=1000000根本就不存在,所以返回結(jié)果也是0:
127.0.0.1:6379> getbit unique:users:2016-04-05 1000000
(integer) 0
3)獲取Bitmaps指定范圍值為1的個數(shù)
bitcount [start] [end]
下面操作計算2016-04-05這天的獨立訪問用戶數(shù)量:
127.0.0.1:6379> bitcount unique:users:2016-04-05
(integer) 5
[start]和[end]代表起始和結(jié)束字節(jié)數(shù),下面操作計算用戶id在第1個字節(jié)到第3個字節(jié)之間的獨立訪問用戶數(shù),對應(yīng)的用戶id是11,15,19。
127.0.0.1:6379> bitcount unique:users:2016-04-05 1 3
(integer) 3
4)Bitmaps間的運算
bitop op destkey key[key....]
bitop是一個復(fù)合操作,它可以做多個Bitmaps的and(交集)、or(并集)、not(非)、xor(異或)操作并將結(jié)果保存在destkey中。假設(shè)2016-04-04訪問網(wǎng)站的userid=1,2,5,9,如圖所示。

下面操作計算出2016-04-04和2016-04-03兩天都訪問過網(wǎng)站的用戶數(shù)
量,如圖3-14所示。
127.0.0.1:6379> bitop and unique:users:and:2016-04-04_03 unique: users:2016-04-03
unique:users:2016-04-03
(integer) 2
127.0.0.1:6379> bitcount unique:users:and:2016-04-04_03
(integer) 2
如果想算出2016-04-04和2016-04-03任意一天都訪問過網(wǎng)站的用戶數(shù)量(例如月活躍就是類似這種),可以使用or求并集,具體命令如下:
127.0.0.1:6379> bitop or unique:users:or:2016-04-04_03 unique:
users:2016-04-03 unique:users:2016-04-03
(integer) 2
127.0.0.1:6379> bitcount unique:users:or:2016-04-04_03
(integer) 6

5)計算Bitmaps中第一個值為targetBit的偏移量
bitpos key targetBit [start] [end]
下面操作計算2016-04-04當(dāng)前訪問網(wǎng)站的最小用戶id:
127.0.0.1:6379> bitpos unique:users:2016-04-04 1
(integer) 1
除此之外,bitops有兩個選項[start]和[end],分別代表起始字節(jié)和結(jié)束字節(jié),例如計算第0個字節(jié)到第1個字節(jié)之間,第一個值為0的偏移量,從圖3-13可以得知結(jié)果是id=0的用戶。
127.0.0.1:6379> bitpos unique:users:2016-04-04 0 0 1
(integer) 0
3. Bitmaps分析
假設(shè)網(wǎng)站有1億用戶,每天獨立訪問的用戶有5千萬,如果每天用集合類型和Bitmaps分別存儲活躍用戶可以得到。

很明顯,這種情況下使用Bitmaps能節(jié)省很多的內(nèi)存空間,尤其是隨著時間推移節(jié)省的內(nèi)存還是非常可觀的。

但Bitmaps并不是萬金油,假如該網(wǎng)站每天的獨立訪問用戶很少,例如只有10萬(大量的僵尸用戶),那么兩者的對比如表3-5所示,很顯然,這時候使用Bitmaps就不太合適了,因為基本上大部分位都是0。

4.redis實現(xiàn)布隆過濾器
redis 在 4.0 的版本中加入了 module 功能,布隆過濾器可以通過 module 的形式添加到 redis 中,所以使用 redis 4.0 以上的版本可以通過加載 module 來使用 redis 中的布隆過濾器。但是這不是最簡單的方式,使用 docker 可以直接在 redis 中體驗布隆過濾器。
> docker run -d -p 6379:6379 --name bloomfilter redislabs/rebloom
> docker exec -it bloomfilter redis-cli
redis 布隆過濾器主要就兩個命令:
- bf.add 添加元素到布隆過濾器中:bf.add urls https://jaychen.cc。
- bf.exists 判斷某個元素是否在過濾器中:bf.exists urls https://jaychen.cc。
上面說過布隆過濾器存在誤判的情況,在 redis 中有兩個值決定布隆過濾器的準(zhǔn)確率:
- error_rate:允許布隆過濾器的錯誤率,這個值越低過濾器的位數(shù)組的大小越大,占用空間也就越大。
- initial_size:布隆過濾器可以儲存的元素個數(shù),當(dāng)實際存儲的元素個數(shù)超過這個值之后,過濾器的準(zhǔn)確率會下降。
redis 中有一個命令可以來設(shè)置這兩個值:
bf.reserve urls 0.01 100
- 第一個值是過濾器的名字。
- 第二個值為 error_rate 的值。
- 第三個值為 initial_size 的值。
使用這個命令要注意一點:執(zhí)行這個命令之前過濾器的名字應(yīng)該不存在,如果執(zhí)行之前就存在會報錯:(error) ERR item exists
