Redis Bitmap介绍及应用

3,069 阅读8分钟

简介

Redis 从2.2.0版本开始新增bit相关命令,部分命令需要3.2.0才能使用

BitMap,即位图,其实也就是 byte 数组,用二进制表示,只有 0 和 1 两个数字。

如图所示:

命令说明

SETBIT

SETBIT key offset value

时间复杂度: O(1)

功能说明

对 key 所储存的字符串值,设置或清除指定偏移量上的位(bit)。

位的设置或清除取决于 value 参数,可以是 0 也可以是 1 。

当 key 不存在时,自动生成一个新的字符串值。

字符串会进行伸展(grown)以确保它可以将 value 保存在指定的偏移量上。当字符串值进行伸展时,空白位置以 0 填充

offset 参数必须大于或等于 0 ,小于 2^32 (bit 映射被限制在 512 MB 之内)

注意

对使用大的 offset 的 SETBIT 操作来说,内存分配可能造成 Redis 服务器被阻塞。具体参考 SETRANGE key offset value 命令,warning(警告)部分。

返回值

指定偏移量原来储存的位

示例

SETBIT bit 10086 1

GETBIT

GETBIT key offset

时间复杂度: O(1)

功能说明

对 key 所储存的字符串值,获取指定偏移量上的位(bit)。

当 offset 比字符串值的长度大,或者 key 不存在时,返回 0 。

返回值

字符串值指定偏移量上的位(bit)。

示例

GETBIT bit 10086

BITCOUNT

BITCOUNT key [start] [end]

时间复杂度: O(N)

功能说明

计算给定字符串中,被设置为 1 的比特位的数量。

一般情况下,给定的整个字符串都会被进行计数,通过指定额外的 start 或 end 参数,可以让计数只在特定的位上进行。

start 和 end 参数的设置和 GETRANGE key start end 命令类似,都可以使用负数值: 比如 -1 表示最后一个字节, -2 表示倒数第二个字节,以此类推。

不存在的 key 被当成是空字符串来处理,因此对一个不存在的 key 进行 BITCOUNT 操作,结果为 0 。

返回值

被设置为 1 的位的数量

示例

BITCOUNT bits

BITPOS

BITPOS key bit [start] [end]

时间复杂度: O(N),其中 N 为位图包含的二进制位数量

功能说明

返回位图中第一个值为 bit 的二进制位的位置。

在默认情况下, 命令将检测整个位图, 但用户也可以通过可选的 start 参数和 end 参数指定要检测的范围

返回值

整数回复

示例

SETBIT bits 3 1

BITOP

BITOP operation destkey key [key …]

时间复杂度: O(N)

功能说明

对一个或多个保存二进制位的字符串 key 进行位元操作,并将结果保存到 destkey 上。

operation 可以是 AND 、 OR 、 NOT 、 XOR 这四种操作中的任意一种:

BITOP AND destkey key [key ...] ,对一个或多个 key 求逻辑并,并将结果保存到 destkey 。

BITOP OR destkey key [key ...] ,对一个或多个 key 求逻辑或,并将结果保存到 destkey 。

BITOP XOR destkey key [key ...] ,对一个或多个 key 求逻辑异或,并将结果保存到 destkey 。

BITOP NOT destkey key ,对给定 key 求逻辑非,并将结果保存到 destkey 。

除了 NOT 操作之外,其他操作都可以接受一个或多个 key 作为输入。

处理不同长度的字符串 当 BITOP 处理不同长度的字符串时,较短的那个字符串所缺少的部分会被看作 0 。

空的 key 也被看作是包含 0 的字符串序列。

注意

BITOP 的复杂度为 O(N) ,当处理大型矩阵(matrix)或者进行大数据量的统计时,最好将任务指派到附属节点(slave)进行,避免阻塞主节点

返回值

保存到 destkey 的字符串的长度,和输入 key 中最长的字符串长度相等。

示例

BITOP AND and-result bits-1 bits-2

BITFIELD

BITFIELD key [GET type offset] [SET type offset value] [INCRBY type offset increment] [OVERFLOW WRAP|SAT|FAIL]

时间复杂度: 每个子命令的复杂度为 O(1)

功能说明

BITFIELD 命令可以将一个 Redis 字符串看作是一个由二进制位组成的数组, 并对这个数组中储存的长度不同的整数进行访问 (被储存的整数无需进行对齐)。 换句话说, 通过这个命令, 用户可以执行诸如 “对偏移量 1234 上的 5 位长有符号整数进行设置”、 “获取偏移量 4567 上的 31 位长无符号整数”等操作。 此外, BITFIELD 命令还可以对指定的整数执行加法操作和减法操作, 并且这些操作可以通过设置妥善地处理计算时出现的溢出情况。

BITFIELD 命令可以在一次调用中同时对多个位范围进行操作: 它接受一系列待执行的操作作为参数, 并返回一个数组作为回复, 数组中的每个元素就是对应操作的执行结果。

比如以下命令就展示了如何对位于偏移量 100 的 8 位长有符号整数执行加法操作, 并获取位于偏移量 0 上的 4 位长无符号整数

注意

使用 GET 子命令对超出字符串当前范围的二进制位进行访问(包括键不存在的情况), 超出部分的二进制位的值将被当做是 0 。

使用 SET 子命令或者 INCRBY 子命令对超出字符串当前范围的二进制位进行访问将导致字符串被扩大, 被扩大的部分会使用值为 0 的二进制位进行填充。 在对字符串进行扩展时, 命令会根据字符串目前已有的最远端二进制位, 计算出执行操作所需的最小长度

返回值

BITFIELD 命令的返回值是一个数组, 数组中的每个元素对应一个被执行的子命令。 需要注意的是, OVERFLOW 子命令本身并不产生任何回复

示例

BITFIELD mykey incrby u2 100 1 OVERFLOW SAT incrby u2 102 1

应用场景

统计每日用户的登录数。每一位标识一个用户ID,当某个用户访问我们的网页或执行了某个操作,就在bitmap中把标识此用户的位设置为1

场景1:1 亿用户,5千万独立

数据类型 每个uid占用空间 需要存储的用户量 全部内存量
set 32位(假设userid用的是整型,实际很多网站用的是长整型) 50,000,000 32位 * 50,000,000 = 200 MB
BitMap 1位 100,000,000 1 位 * 100,000,000 = 12.5 MB

场景2:1 亿用户,10万独立

数据类型 每个uid占用空间 需要存储的用户量 全部内存量
set 32位(假设userid用的是整型,实际很多网站用的是长整型) 1,000,000 32位 * 1,000,000 = 4 MB
BitMap 1位 100,000,000 1 位 * 100,000,000 = 12.5 MB

可以发现,如果独立用户数量很多,使用 BitMap 明显更有优势,能节省大量的内存。但如果独立用户数量较少,还是建议使用 set 存储,BitMap 会产生多余的存储开销

使用经验

type = string,BitMap 是 sting 类型,最大 512 MB。

注意 setbit 时的偏移量,可能有较大耗时

位图不是绝对好。

使用举例

使用 bitmap 实现用户上线次数统计 Bitmap 对于一些特定类型的计算非常有效。

假设现在我们希望记录自己网站上的用户的上线频率,比如说,计算用户 A 上线了多少天,用户 B 上线了多少天,诸如此类,以此作为数据,从而决定让哪些用户参加 beta 测试等活动 —— 这个模式可以使用 SETBIT key offset value 和 BITCOUNT key [start] [end] 来实现。

比如说,每当用户在某一天上线的时候,我们就使用 SETBIT key offset value ,以用户名作为 key ,将那天所代表的网站的上线日作为 offset 参数,并将这个 offset 上的为设置为 1 。

举个例子,如果今天是网站上线的第 100 天,而用户 peter 在今天阅览过网站,那么执行命令 SETBIT peter 100 1 ;如果明天 peter 也继续阅览网站,那么执行命令 SETBIT peter 101 1 ,以此类推。

当要计算 peter 总共以来的上线次数时,就使用 BITCOUNT key [start] [end] 命令:执行 BITCOUNT peter ,得出的结果就是 peter 上线的总天数。

性能

前面的上线次数统计例子,即使运行 10 年,占用的空间也只是每个用户 10*365 比特位(bit),也即是每个用户 456 字节。对于这种大小的数据来说, BITCOUNT key [start] [end] 的处理速度就像 GET key 和 INCR key 这种 O(1) 复杂度的操作一样快。

如果你的 bitmap 数据非常大,那么可以考虑使用以下两种方法:

将一个大的 bitmap 分散到不同的 key 中,作为小的 bitmap 来处理。使用 Lua 脚本可以很方便地完成这一工作。

使用 BITCOUNT key [start] [end] 的 start 和 end 参数,每次只对所需的部分位进行计算,将位的累积工作(accumulating)放到客户端进行,并且对结果进行缓存 (caching)。