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
只统计位图指定字节范围内的二进制位
在默认情况下,BITCOUNT 命令将对位图包含的所有字节中的二进制位进行统计,但在有需要的情况下,用户也可以通过可选的 start 参数和 end 参数,让 BITCOUNT 只对指定字节范围内的二进制位进行统计:
BITCOUNT bitmap [start end]
注意 start 参数和 end 参数与本章之前介绍的 SETBIT 命令和 GETBIT 命令的 offset 参数并不相同,这两个参数是用来指定字节偏移量而不是二进制位偏移量的。位图的字节偏移量与 Redis 其他数据结构的偏移量一样,都是从 0 开始的:位图第一个字节的偏移量为 0,第二个字节的偏移量为 1,第三个字节的偏移量为 2,以此类推。

举个例子,对于图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 命令的作用,就很容易出现上述的问题。

使用负数偏移量定义统计范围
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 的字节索引。

示例:用户行为记录器
为了对用户的行为进行分析并借此改善服务质量,很多网站都会对用户在网站上的一举一动进行记录。比如记录哪些用户登录了网站,哪些用户发表了新的文章,哪些用户进行了消费,诸如此类。
为此,我们可以使用本书前面介绍过的集合或者 HyperLogLog 来记录所有执行了指定行为的用户,但这两种做法都有相应的缺陷:
-
如果使用集合来记录执行了指定行为的用户,那么集合的体积就会随着用户数量的增多而变大,从而消耗大量内存。
-
虽然使用 HyperLogLog 来记录用户行为这一做法可以节约大量内存,但由于 Hyper-LogLog 是一个概率算法,所以它只能给出执行了指定行为的人数的估算值,并且无法准确地判断一个用户是否执行了指定行为,这会给一些需要精确结果的分析算法带来麻烦。
为了尽可能地节约内存,并且精确地记录特定用户是否执行了指定的行为,我们可以使用以下方法:
-
对于每项行为,一个用户要么执行了该行为,要么没有执行该行为,只有两种可能,因此用户是否执行了指定行为这一信息可以通过一个二进制位来记录。
-
通过将用户 ID 与位图中的二进制位偏移量进行一对一映射,我们可以使用一个位图来记录所有执行了指定行为的用户:比如偏移量为 10086 的二进制位就负责记录 ID 为 10086 的用户信息,而偏移量为 12345 的二进制位则负责记录 ID 为 12345 的用户信息,以此类推。
-
每当用户执行指定行为时,我们就调用 SETBIT 命令,将用户在位图中对应的二进制位的值设置为 1。
-
通过调用 GETBIT 命令并判断用户对应的二进制位的值是否为 1,我们可以知道用户是否执行了指定的行为。
-
通过对位图执行 BITCOUNT 命令,我们可以知道有多少用户执行了指定行为。
代码清单8-1展示了使用这一原理实现的用户行为记录器程序。
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 的优点,既可以像集合那样准确地判断特定用户是否执行了指定行为,又可以像 HyperLogLog 那样大量减少内存消耗:对于每项行为,使用这个程序去记录 100 万个用户的信息只需要耗费 125KB 内存,而记录 1000 万个用户的信息也只需要 1.25MB 内存。
作为例子,以下代码展示了如何使用这个程序去记录用户的登录行为:
>>> from redis import Redis
>>> from action_recorder import ActionRecorder
>>> client = Redis()
>>> login_action = ActionRecorder(client, "login")
>>> login_action.perform_by(10086) # 对已登录用户进行记录
>>> login_action.perform_by(255255)
>>> login_action.perform_by(987654321)
>>> login_action.is_performed_by(10086) # ID为10086的用户登录了
True
>>> login_action.is_performed_by(555) # ID为555的用户没有登录
False
>>> login_action.count_performed() # 共有3个用户执行了登录操作
3