如果使用一個set來記錄當(dāng)天活躍的用戶,當(dāng)用戶量非常大時會浪費(fèi)非常多的空間。因此redis提供了位圖(bitmap),讓用戶可以對每一位進(jìn)行單獨(dú)操作。下面本篇文章就來帶大家了解一下redis中的位圖,希望對大家有所幫助!
位圖
位圖,即大量bit組成的一個數(shù)據(jù)結(jié)構(gòu)(每個bit只能是0和1),主要適合在一些場景下,進(jìn)行空間的節(jié)省,并有意義的記錄數(shù)據(jù),
例如一些大量的bool類型的存取,一個用戶365天的簽到記錄,簽到了是1,沒簽到是0,如果用普通的key/value進(jìn)行存儲,當(dāng)用戶量很大的時候,需要的存儲空間是很大的。
如果使用位圖進(jìn)行存儲,一年365天,用365個bit就可以存儲,365個bit換算成46個字節(jié)(一個稍長的字符串),如此就節(jié)省了很多的存儲空間,
位圖的本質(zhì)其實(shí)是一個普通的字符串,也就是byte數(shù)組,可以使用get/set直接獲取和設(shè)置整個位圖的內(nèi)容,也可以使用 getbit/setbit 將byte數(shù)組看成bit數(shù)組來處理。【相關(guān)推薦:Redis視頻教程】
使用位操作設(shè)置字符串
正常設(shè)置字符串都使用set命令,下面我們使用setbit設(shè)置一下位數(shù)組,最后以獲取字符串的形式獲取,
首先我們獲取一下h、e兩個ASCII碼使用二進(jìn)制的表示如下,
可以看到h的二進(jìn)制碼是 01101000 , e的二進(jìn)制碼是 01100101,我們只需要注意bit是1的位置,然后進(jìn)行setbit,
需要注意的是,位數(shù)組的順序和字符的位順序是反的,根據(jù)這個原則,我們算出 h字符 每個1的位置分別是1/2/4, e字符的則是 9/10/13/15,
所以我們將使用setbit設(shè)置一個位數(shù)組,并在每個位置上(1/2/4/9/10/13/15)設(shè)置對應(yīng)的1,
setbit?data?1?1 setbit?data?2?1 setbit?data?4?1 setbit?data?9?1 setbit?data?10?1 setbit?data?13?1 setbit?data?15?1
零存整取
最后直接 get data這個key,會發(fā)現(xiàn)正好得到he,
setbit + get 的組合稱為 零存整取,零存就是一個bit一個bit的設(shè)置,整取就是通過key名字,直接get出來所有的數(shù)據(jù),
同樣,我們還可以進(jìn)行 零存零取,整存零取,整存就是直接使用字符串設(shè)置整個位數(shù)組,零取則是通過bit的位置,進(jìn)行bit的獲取。
零存零取
可以看到,我們根據(jù)setbit,對key叫做w的位數(shù)組進(jìn)行bit設(shè)置,只設(shè)置了1/2/4這3個位置的值為1,下圖中有g(shù)etbit w 3, 獲取第三個位置的值,此時默認(rèn)是0,如果從業(yè)務(wù)角度觸發(fā),可以理解為,一共簽到4天,第三天沒有進(jìn)行簽到,
整存零取
下午所示,我們對w的這個key,直接set了一個h字符,隨后通過getbit獲取w的位數(shù)組里的每個bit,可以看到獲取出來的內(nèi)容和上面h字符的二進(jìn)制內(nèi)容相同 1/2/4的位置是1,其余是0
注意
-
redis的位數(shù)組是自動擴(kuò)充的,如果設(shè)置的某個偏移位置超出了現(xiàn)有的內(nèi)容范圍,就會自動將位數(shù)組進(jìn)行零擴(kuò)充,即擴(kuò)容的位默認(rèn)都是0值。
-
如果對應(yīng)位的字節(jié)是不可打印字符,redis-cli將會顯示該字符的十六進(jìn)制形式。
-
一個字節(jié)是8個bit(位),要區(qū)分字節(jié)和位。
統(tǒng)計(jì)和查找 (bitcount/bitpos)
redis提供了 統(tǒng)計(jì)指令 bitcount ?和 ?位圖查找指令 bitpos ,
bitcount用來統(tǒng)計(jì)指定位置范圍內(nèi)1的個數(shù),bitpos用來查找指定范圍內(nèi)出現(xiàn)的第一個0或1。
我們可以通過bitcount統(tǒng)計(jì)用戶一共簽到了多少天,通過bitpos指令查找用戶從哪一天開始第一次簽到,
如果指定了范圍參數(shù)[start, end],就可以統(tǒng)計(jì)在某個時間范圍內(nèi)用戶簽到了多少天以及用戶自某天以后的哪天開始簽到,
但是需要注意的是,start和end參數(shù)是字節(jié)索引,也就是說,指定的位范圍必須是8的倍數(shù),
而不能任意指定,所以我們無法直接計(jì)算某個月內(nèi)用戶簽到了多少天,如果需要計(jì)算的話,
可以使用getrange命令取出該月覆蓋的字節(jié)內(nèi)容,然后在內(nèi)存中進(jìn)行統(tǒng)計(jì),例如2月覆蓋了10-12個字節(jié),就使用 getrange w 8 12 。
127.0.0.1:6379>?set?w?hello???? OK 127.0.0.1:6379>?bitcount?w??????#?所有字符中有多少個1 (integer)?21 127.0.0.1:6379>?bitcount?w?0?0???#?第一個字符中?1?的位數(shù) (integer)?3 127.0.0.1:6379>?bitcount?w?0?1???#?前兩個字符中?1?的位數(shù) (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
bitfield
之前介紹的 setbit / getbit 指定位的值都是單個位,如果要一次操作多個位,就必須使用管道來處理,
在redis3.2以后,提供了bitfield指令,可以一次對多個位進(jìn)行操作,bitfield有三個子指令,分別是get/set/incrby, 都可以對指定位片段進(jìn)行讀寫,
但是最多只能處理64個連續(xù)的位,如果超過64位,就需要使用多個子指令,bitfield可以一次執(zhí)行多個子指令。
示例
下面對下圖的位數(shù)組使用 bitfield 做一些操作
127.0.0.1:6379>?bitfield?w?get?u4?0???#?從第1個位開始取4個位,結(jié)果是無符號數(shù)(u) 1)?(integer)?6 127.0.0.1:6379>?bitfield?w?get?u3?2???#?從第3個位開始取3個位,結(jié)果是無符號數(shù)(u) 1)?(integer)?5 127.0.0.1:6379>?bitfield?w?get?i4?0???#?從第1個位開始取4個位,結(jié)果是有符號數(shù)(i) 1)?(integer)?6 127.0.0.1:6379>?bitfield?w?get?i3?2???#?從第3個位開始取3個位,結(jié)果是有符號數(shù)(i) 1)?(integer)?-3
有符號數(shù)是指獲取的位數(shù)組中的第一個位是符號位,剩下的才是值,如果第一位是1,就是負(fù)數(shù),
無符號數(shù)表示非負(fù)數(shù),沒有符號位,獲取的位數(shù)組全部都是值,有符號數(shù)最多可以獲取64位,
無符號數(shù)只能獲取63位,因?yàn)閞edis協(xié)議中的integer是有符號數(shù),最大64位,不能傳遞64位的無符號值,
如果超出位數(shù)限制,redis就會告訴你參數(shù)錯誤。
上面的指令可以合并成一條指令,可以看到得到的結(jié)果是一樣的,
bitfield?w?get?u4?0?get?u3?2?get?i4?0?get?i3?2
set修改
我們從第9個位開始,用8個無符號數(shù)替換已經(jīng)存在的8個位,其實(shí)就是把第二個字符替換了,由e變成a(它的ASCII碼是97),可以看到結(jié)果也變成了 hallo
127.0.0.1:6379>?bitfield?w?set?u8?8?97 1)?(integer)?101 127.0.0.1:6379>?get?w "hallo"
incrby
incrby對指定范圍的位進(jìn)行自增操作,即++,這可能會發(fā)生溢出,如果增加了正數(shù),會出現(xiàn)上溢出,如果增加的是負(fù)數(shù),會出現(xiàn)下溢出,
redis默認(rèn)的處理是折返,即如果出現(xiàn)了溢出,就將溢出的符號位丟掉,例如,如果是8位無符號數(shù)255,加1后就全部變成0,如果是8位有符號數(shù)127,加1后就溢出變成-128。
依然根據(jù)hello字符,來演示一下 incrby
127.0.0.1:6379>?set?w?hello OK 127.0.0.1:6379>?bitfield?w?get?u4?2?????#?從第3位開始取4個無符號整數(shù),第一次是10 1)?(integer)?10 127.0.0.1:6379>?bitfield?w?incrby?u4?2?1 1)?(integer)?11 127.0.0.1:6379>?bitfield?w?incrby?u4?2?1 1)?(integer)?12 127.0.0.1:6379>?bitfield?w?incrby?u4?2?1 1)?(integer)?13 127.0.0.1:6379>?bitfield?w?incrby?u4?2?1 1)?(integer)?14 127.0.0.1:6379>?bitfield?w?incrby?u4?2?1 1)?(integer)?15 127.0.0.1:6379>?bitfield?w?incrby?u4?2?1???#到這里的時候,已經(jīng)溢出折返成0了 1)?(integer)?0
bitfield指令提供溢出策略子指令 overflow,用戶可以選擇溢出行為,默認(rèn)是折返(wrap),還可以選擇失敗(fail) 即報(bào)錯不執(zhí)行,還有飽和截?cái)?sat) 即超過范圍就停留在最大或最小值,
overflow指令只影響接下來的第一條指令,這條指令執(zhí)行完以后,溢出策略就會變成默認(rèn)值 折返(wrap)。
飽和截?cái)?/strong>
127.0.0.1:6379>?set?w?hello OK 127.0.0.1:6379>?bitfield?w?overflow?sat?incrby?u4?2?1 1)?(integer)?11 127.0.0.1:6379>?bitfield?w?overflow?sat?incrby?u4?2?1 1)?(integer)?12 127.0.0.1:6379>?bitfield?w?overflow?sat?incrby?u4?2?1 1)?(integer)?13 127.0.0.1:6379>?bitfield?w?overflow?sat?incrby?u4?2?1 1)?(integer)?14 127.0.0.1:6379>?bitfield?w?overflow?sat?incrby?u4?2?1 1)?(integer)?15 127.0.0.1:6379>?bitfield?w?overflow?sat?incrby?u4?2?1?#?接下來的都將是保持最大值 1)?(integer)?15 127.0.0.1:6379>?bitfield?w?overflow?sat?incrby?u4?2?1 1)?(integer)?15 127.0.0.1:6379>?bitfield?w?overflow?sat?incrby?u4?2?1 1)?(integer)?15
失敗不執(zhí)行
127.0.0.1:6379>?set?w?hello OK 127.0.0.1:6379>?bitfield?w?overflow?fail?incrby?u4?2?1 1)?(integer)?11 127.0.0.1:6379>?bitfield?w?overflow?fail?incrby?u4?2?1 1)?(integer)?12 127.0.0.1:6379>?bitfield?w?overflow?fail?incrby?u4?2?1 1)?(integer)?13 127.0.0.1:6379>?bitfield?w?overflow?fail?incrby?u4?2?1 1)?(integer)?14 127.0.0.1:6379>?bitfield?w?overflow?fail?incrby?u4?2?1 1)?(integer)?15 127.0.0.1:6379>?bitfield?w?overflow?fail?incrby?u4?2?1?#?接下來的都是失敗 1)?(nil) 127.0.0.1:6379>?bitfield?w?overflow?fail?incrby?u4?2?1 1)?(nil) 127.0.0.1:6379>?bitfield?w?overflow?fail?incrby?u4?2?1 1)?(nil)
get/set/incrby一起執(zhí)行
127.0.0.1:6379>?bitfield?w?set?u4?1?0?get?u4?1?incrby?u4?2?1 1)?(integer)?0 2)?(integer)?0 3)?(integer)?1 127.0.0.1:6379>?get?w "x04ello"
更多編程相關(guān)知識,請?jiān)L問:Redis視頻教程!!