一、場景
在很多時候我們會遇到用戶簽到的場景,每天用戶進入應用時,需要獲取用戶當天的簽到狀態,如果沒簽到,用戶可以進行簽到,並且得到相關的獎勵。我們可能需要每天的簽到情況,必要的時候可能還需要統計一下每天用戶簽到人數。
我們用Redis的Set數據結構可以輕松實現這個功能——以日期為key,以用戶ID(對應着數據庫的primary id)組成的集合為value,每當需要查詢某個用戶的簽到狀態時,只需要使用命令SISMEMBER key member
就可以輕易得到想要的結果;用戶簽到時,使用命令SADD key member
把用戶ID添加到相應的日期中;統計某天用戶的簽到人數,可以用命令SCARD key
。
以上的做法操作簡便,易於理解,但是本篇要介紹的是另一種做法,使用Redis的位操作(bitmap)。
我們都知道數據在機器上存儲的最小單元是位(bit),1位可以存儲0和1兩種狀態。這里的場景需要存儲正是簽到和未簽到兩種狀態,因此一個用戶只需要占用1位,也就是用位操作比用集合操作要省很多空間,下面先說一下位操作的方式,最后會給出兩種方式的內存占用對比。
Redis提供了一組位操作相關的指令,這里我們關注下面三個:
- BITCOUNT key [start end]
返回key的開始位置start到結束位置end之間位值為1的數量,如果key不存在,返回0;如果不指定start和end,返回整個key的位值為1的數量。
- GETBIT key offset
返回key的第offset位的位值。
- SETBIT key offset value
把key的第offset位的值設置為value,value只能是0或1。
說明一下Redis的位操作的偏移量(offset)是從0開始算起的,而且最左邊那位是第0位,這與數值的二進制有點不同(數值的二進制最右邊那位是第0位)。
二、解決方案
有了以上兩組操作之后,再回到我們的場景,這里假定我們有500w注冊用戶,日活又主動簽到的用戶只有30w,新用戶的活躍度更高。如果使用redis的Set的操作,那么我們每天需要存儲的數據就是這30w用戶的id,一般來說,新注冊的用戶的活躍度會比舊用戶的活躍度要高,為了方便測試,我們假定每天活躍的用戶就是id最大的30w用戶。下面是兩種方案的具體操作:
2.1、使用Set存儲數據
先准備30w條redis指令並且寫到一個data.txt文件中,格式如下:
SADD sign_in_20200113 4700001
SADD sign_in_20200113 4700002
SADD sign_in_20200113 4700003
SADD sign_in_20200113 4700004
SADD sign_in_20200113 4700005
...
然后通過redis的管道命令來把數據寫到redis:
cat data.txt | redis-cli --pipe
完成后可以看一下數據是否成功寫到redis中:
127.0.0.1:6379> scard sign_in_20200113
(integer) 300000
指定的key已經有30w個用戶簽到,同時用info命令查看一下這時redis的占用內存:
# Memory
used_memory:21604936
used_memory_human:20.60M
占用的內存大概是20M。
然后我們需要查詢一個用戶的簽到狀態和用戶簽到都非常方便。
2.2、使用bitmap存儲數據
接下來我們再用bitmap進行操作,同樣我們准備好相關的redis指令,如下:
SETBIT sign_in_20200113 4700001 1
SETBIT sign_in_20200113 4700002 1
SETBIT sign_in_20200113 4700003 1
SETBIT sign_in_20200113 4700004 1
SETBIT sign_in_20200113 4700005 1
...
完成后我們可以通過bitcount命令查看一下簽到人數:
127.0.0.1:6379> bitcount sign_in_20200113
(integer) 300000
這時再看一下占用內存的情況:
# Memory
used_memory:2088200
used_memory_human:1.99M
只占了大約2M,和使用Set的方式相差了10倍!
三、方案對比
- 使用Set的方式所占用的內存只與數量相關,和存儲哪些id無關
- 使用bitmap的方式所占用的內存與數量沒有絕對的關系,而是與最高位有關。比如假設id為500w的用戶簽到了,那么從1號用戶到4999999號用戶不管是否簽到,所占的內存都是500w個bit,這也是bitmap的最壞情況,假如上述場景是1號用戶到30w號用戶簽到,那么使用的內存就只是30w個bit,大約只占了940K,比最壞情況還要省一半的空間。
- 使用bitmap存儲,最大的offset是2^32-1,也就是一個bitmap格式的key最大可以存儲512M的數據。
- 使用bitmap存儲的時候,有可能一開始是id較小的用戶簽到了,后面會有id較大的用戶簽到,這種情況下key的長度需要動態擴展,這需要花費一定的時間。在MBP2010上給offset為232-1分配512M的內存大約需要300ms,給offset為230-1分配128M的內存大約需要80ms,offset為228-1分配32M需要約30ms,offset為226-1分配8M大約需要8ms。當然,如果分配了可以容納高位的空間后,使用低位時就不需要再擴容,比如一開始就通過setbit設置了第500w位的值,后面再使用offset小於500w的位都可以直接使用。
- 如果需要另外存儲,可以每天用定時任務把數據寫在需要的地方,比如MySQL。
四、適用場景
redis的bitmap操作雖然優點明顯,但局限性也是顯而易見的。因為它使用1bit來存儲數據,所以只適用存儲只有兩個狀態的數據,比如用戶簽到,資源(視頻、文章、商品)的已讀或未讀狀態。
關於redis的bitmap更多用法,可以參考官方文檔。