Redis的位图(bitmap)是由多个二进制位组成的数组,数组中的每个二进制位都有与之对应的偏移量(也称索引),用户通过这些偏移量可以对位图中指定的一个或多个二进制位进行操作。
图8-1展示了一个包含8个二进制位的位图示例,这个位图存储的值为10010100。
图8-1 位图示例
Redis为位图提供了一系列操作命令,通过这些命令,用户可以:
-
为位图指定偏移量上的二进制位设置值,或者获取位图指定偏移量上的二进制位的值。
-
统计位图中有多少个二进制位被设置成了1。
-
查找位图中第一个被设置为指定值的二进制位并返回它的偏移量。
-
对一个或多个位图执行逻辑并、逻辑或、逻辑异或以及逻辑非运算。
-
将指定类型的整数存储到位图中。
接下来将对以上提到的各个位图命令进行介绍,并展示如何使用位图去实现用户行为记录器、0-1矩阵存储程序以及能够有效地节约内存的整数计数器。
1.1 SETBIT:设置二进制位的值
通过使用SETBIT命令,用户可以为位图指定偏移量上的二进制位设置值:
SETBIT bitmap offset value
SETBIT命令在对二进制位进行设置之后,将返回二进制位被设置之前的旧值作为结果。
举个例子,如果我们想要将位图bitmap001的值设置成10010100,那么可以执行以下3个命令:
redis> SETBIT bitmap001 0 1
(integer) 0 -- 二进制位原来的值为0
redis> SETBIT bitmap001 3 1
(integer) 0
redis> SETBIT bitmap001 5 1
(integer) 0
图8-2展示了以上3个命令的执行过程。
图8-2 SETBIT命令对位图的修改过程
1.1.1 位图的扩展
当用户执行SETBIT命令尝试对一个位图进行设置的时候,如果位图不存在,或者位图当前的大小无法满足用户想要执行的设置操作,那么Redis将对被设置的位图进行扩展,使得位图可以满足用户的设置请求。
因为Redis对位图的扩展操作是以字节为单位进行的,所以扩展之后的位图包含的二进制位数量可能会比用户要求的稍微多一些,并且在扩展位图的同时,Redis还会将所有未被设置的二进制位的值初始化为0。
比如,如果用户执行以下命令,对尚未存在的位图bitmap002在偏移量10之上的二进制位进行设置:
redis> SETBIT bitmap002 10 1
(integer) 0
那么Redis创建出的位图并不会只有11个二进制位,而是有两个字节共16个二进制位,如图8-3所示。
图8-3 包含16个二进制位的位图bitmap002
从这个图我们也可以看到,除了偏移量为10的二进制位之外,其他所有未被设置的二进制位都被初始化成了0。
1.1.2 偏移量只能为正数
与一些Redis命令可以使用负数作为偏移量的做法不同,SETBIT命令只能使用正数偏移量,尝试输入负数作为偏移量将引发一个错误:
redis> SETBIT bitmap001 -1 1
(error) ERR bit offset is not an integer or out of range
1.1.3 其他信息
复杂度:O(1)。
版本要求:SETBIT命令从Redis 2.2.0版本开始可用。
1.2 GETBIT:获取二进制位的值
使用GETBIT命令,用户可以获取位图指定偏移量上的二进制位的值:
GETBIT bitmap offset
与SETBIT命令一样,GETBIT命令也只能接受正数作为偏移量。
举个例子,对于值为10010100的位图bitmap001来说,可以通过执行以下命令,分别获取它在偏移量0、偏移量3、偏移量5以及偏移量7上的二进制位的值:
redis> GETBIT bitmap001 0
(integer) 1
redis> GETBIT bitmap001 3
(integer) 1
redis> GETBIT bitmap001 5
(integer) 1
redis> GETBIT bitmap001 7
(integer) 0
图8-4展示了这4个GETBIT命令对bitmap001进行取值的过程。
图8-4 GETBIT命令的执行过程
1.2.1 处理范围之外的偏移量
如果用户输入的偏移量超过了位图目前拥有的最大偏移量,那么GETBIT命令将返回0作为结果:
redis> GETBIT bitmap001 100 # bitmap001只包含8个二进制位
(integer) 0 # 100不在它的有效偏移量范围之内
换句话说,GETBIT命令会把位图中所有不存在的二进制位的值都看作0。
1.2.2 其他信息
复杂度:O(1)。
版本要求:GETBIT命令从Redis 2.2.0版本开始可用。
1.3 BITCOUNT:统计被设置的二进制位数量
用户可以通过执行BITCOUNT命令统计位图中值为1的二进制位数量:
BITCOUNT key
比如,对于值为10010100的位图bitmap001,可以通过执行以下命令来统计它有多少个二进制位被设置成了1:
redis> BITCOUNT bitmap001
(integer) 3 -- 这个位图有3个二进制位被设置成了1
而对于值为0000000000100000的位图bitmap002,可以通过执行以下命令来统计它有多少个二进制位被设置成了1:
redis> BITCOUNT bitmap002
(integer) 1 -- 这个位图只有1个二进制位被设置成了1
1.3.1 只统计位图指定字节范围内的二进制位
在默认情况下,BITCOUNT命令将对位图包含的所有字节中的二进制位进行统计,但在有需要的情况下,用户也可以通过可选的start参数和end参数,让BITCOUNT只对指定字节范围内的二进制位进行统计:
BITCOUNT bitmap [start end]
注意start参数和end参数与本章之前介绍的SETBIT命令和GETBIT命令的offset参数并不相同,这两个参数是用来指定字节偏移量而不是二进制位偏移量的。位图的字节偏移量与Redis其他数据结构的偏移量一样,都是从0开始的:位图第一个字节的偏移量为0,第二个字节的偏移量为1,第三个字节的偏移量为2,以此类推。
图8-5 包含24个二进制位的位图bitmap003
举个例子,对于图8-5所示的包含3个字节共24个二进制位的位图bitmap003来说,我们可以通过执行以下命令统计出它的第一个字节里面有多少个二进制位被设置成了1:
redis> BITCOUNT bitmap003 0 0
(integer) 6
如果我们想要知道bitmap003的第一个字节和第二个字节中有多少个二进制位被设置成了1,那么可以执行以下命令:
redis> BITCOUNT bitmap003 0 1
(integer) 9
如果我们想要知道bitmap003的第三个字节中有多少个二进制位被设置成了1,那么可以执行以下命令:
redis> BITCOUNT bitmap003 2 2
(integer) 4
图8-6展示了以上3个BITCOUNT命令在执行期间都统计了哪些二进制位。
不要把BITCOUNT的字节偏移量当作二进制位偏移量
再次提醒,BITCOUNT命令的start参数和end参数定义的是字节偏移量范围,而不是二进制位偏移量范围。
很多Redis用户在刚开始使用BITCOUNT命令的时候,都会误以为BITCOUNT接受的是二进制位偏移量范围,比如想要使用BITCOUNT bitmap 02去统计位图的前3个二进制位,但实际上统计的却是位图前3个字节包含的所有二进制位,诸如此类。
如果不认真地了解BITCOUNT命令的作用,就很容易出现上述的问题。
图8-6 3个BITCOUNT命令执行期间统计的二进制位
1.3.2 使用负数偏移量定义统计范围
BITCOUNT命令的start参数和end参数的值除了可以是正数之外,还可以是负数。
以下是一些使用负数偏移量对位图bitmap003的指定字节进行统计的例子:
redis> BITCOUNT bitmap003 -1 -1 -- 统计最后一个字节
(integer) 4
redis> BITCOUNT bitmap003 -2 -2 -- 统计倒数第二个字节
(integer) 3
redis> BITCOUNT bitmap003 -3 -3 -- 统计倒数第三个字节
(integer) 6
图8-7分别以正数和负数两种形式展示了位图bitmap003的字节索引。
图8-7 带有正数字节索引和负数字节索引的位图bitmap003
1.3.3 其他信息
复杂度:O(N),其中N为被统计字节的数量。
版本要求:BITCOUNT命令从Redis 2.6.0版本开始可用。
示例:用户行为记录器
为了对用户的行为进行分析并借此改善服务质量,很多网站都会对用户在网站上的一举一动进行记录。比如记录哪些用户登录了网站,哪些用户发表了新的文章,哪些用户进行了消费,诸如此类。
为此,我们可以使用本书前面介绍过的集合或者HyperLogLog来记录所有执行了指定行为的用户,但这两种做法都有相应的缺陷:
-
如果使用集合来记录执行了指定行为的用户,那么集合的体积就会随着用户数量的增多而变大,从而消耗大量内存。
-
虽然使用HyperLogLog来记录用户行为这一做法可以节约大量内存,但由于Hyper-LogLog是一个概率算法,所以它只能给出执行了指定行为的人数的估算值,并且无法准确地判断一个用户是否执行了指定行为,这会给一些需要精确结果的分析算法带来麻烦。
为了尽可能地节约内存,并且精确地记录特定用户是否执行了指定的行为,我们可以使用以下方法:
-
对于每项行为,一个用户要么执行了该行为,要么没有执行该行为,只有两种可能,因此用户是否执行了指定行为这一信息可以通过一个二进制位来记录。
-
通过将用户ID与位图中的二进制位偏移量进行一对一映射,我们可以使用一个位图来记录所有执行了指定行为的用户:比如偏移量为10086的二进制位就负责记录ID为10086的用户信息,而偏移量为12345的二进制位则负责记录ID为12345的用户信息,以此类推。
-
每当用户执行指定行为时,我们就调用SETBIT命令,将用户在位图中对应的二进制位的值设置为1。
-
通过调用GETBIT命令并判断用户对应的二进制位的值是否为1,我们可以知道用户是否执行了指定的行为。
-
通过对位图执行BITCOUNT命令,我们可以知道有多少用户执行了指定行为。
代码清单8-1展示了使用这一原理实现的用户行为记录器程序。
代码清单8-1 用户行为记录器:/bitmap/action_recorder.py
def make_action_key(action):
return "action_recorder::" + action
class ActionRecorder:
def __init__(self, client, action):
self.client = client
self.bitmap = make_action_key(action)
def perform_by(self, user_id):
"""
记录执行了指定行为的用户
"""
self.client.setbit(self.bitmap, user_id, 1)
def is_performed_by(self, user_id):
"""
检查给定用户是否执行了指定行为,执行则返回True,未执行则返回False
"""
return self.client.getbit(self.bitmap, user_id) == 1
def count_performed(self):
"""
返回执行了指定行为的用户人数
"""
return self.client.bitcount(self.bitmap)
这个使用位图实现的行为记录器同时具备了集合和HyperLogLog的优点,既可以像集合