程序员求职经验分享与学习资料整理平台

网站首页 > 文章精选 正文

bitmap很强大,但要谨慎使用(bitmaps)

balukai 2025-06-08 19:22:53 文章精选 2 ℃

bitmap不是一个实际的数据类型,而是在String类型上定义的一组面向位的操作。由于字符串是二进制安全的,其最大长度为512MB,因此适合设置多达2^32个不同的位。

bitmap最大的优点之一是,在存储信息时,它们通常可以极大地节省空间。例如,在不同用户由增量用户ID表示的系统中,可以仅使用512MB的内存记住40亿用户的一个比特信息

可以把Bitmap理解成一个以位为单位的数组,数组的每个单元只能存储0和1,数组的下标在Bitmap中叫做偏移量。

常用命令

1. setbit

SETBIT key offset value

可用版本: 2.2.0以后

复杂度: O(1)

在offset设置相应的比特值,不是0就是1,offset从0开始。

127.0.0.1:6379> setbit mybitmap 10 1  #将第10个位置的bit位设置为1
(integer) 0

当键不存在时,将创建一个新的字符串值。我们暂可以理解为创建了一个以bit单位的数组,数据的长度要看offset是多少,数组长度是以8倍bit(一个字节)进行扩充的,如果offset=2,需要一个字节就可以,即8个bit位,数组长度为8,如果offset=8,需要分配2个字节才能存储,即16个bit位,数组长度为16,以此类推。offset数必须大于或等于0,并且小于2^32(bitmap限制为512MB)。设置一个offset时,要把offset之前的内存全部分配好了。

127.0.0.1:6379> del mybitmap
(integer) 1
127.0.0.1:6379> setbit mybitmap 2 1
(integer) 0
127.0.0.1:6379> strlen mybitmap         #分配了1字节的空间
(integer) 1
127.0.0.1:6379> setbit mybitmap 7 1
(integer) 0
127.0.0.1:6379> strlen mybitmap         #分配了1字节的空间
(integer) 1
127.0.0.1:6379> setbit mybitmap 8 1   
(integer) 0
127.0.0.1:6379> strlen mybitmap        #当offset到8时,分配了2个字节的空间
(integer) 2
127.0.0.1:6379> setbit mybitmap 16 1   
(integer) 0
127.0.0.1:6379> strlen mybitmap        #当offset到16时,分配了3个字节的空间
(integer) 3

警告:当设置最后一个bit位(偏移量等于2^32-1),Redis需要分配所有中间内存,这些内存可能会在一段时间内阻塞服务器。例如设置位数2^32-1(512MB分配)需要大约300ms,设置位数2*30-1(128MB分配)大约80ms,设置位号2^28-1(32MB分配)花费大约30ms,设置位号2^26-1(8MB分配)大约8ms。请注意,一旦完成了第一次分配,对同一key的SETBIT的后续调用将不会有分配开销。

在某些情况下,您需要同时设置单个bitmap的所有位,例如将其初始化为默认的非零值。可以通过多次调用SETBIT命令来实现这一点,每个需要设置的位一次。其实可以使用单个SET命令来设置整个位图。

bitmap是在String类型上定义的一组面向位的操作。这意味着bitmap可以与字符串命令一起使用,最重要的是可以与SET和GET一起使用。

因为Redis的字符串是二进制安全的,所以bitmap通常被编码为字节流。字符串的第一个字节对应bitmap的偏移0..7,第二个字节对应8..15范围,依此类推。

字符串"ab"二进制为"0110000101100010",可以用字符串的方式设置bitmap,也可以用bitmap方式设置字符串。

127.0.0.1:6379> del mybitmap
(integer) 1
127.0.0.1:6379> setbit mybitmap 1 1
(integer) 0
127.0.0.1:6379> setbit mybitmap 2 1
(integer) 0
127.0.0.1:6379> setbit mybitmap 7 1
(integer) 0
127.0.0.1:6379> setbit mybitmap 9 1
(integer) 0
127.0.0.1:6379> setbit mybitmap 10 1
(integer) 0
127.0.0.1:6379> setbit mybitmap 14 1
(integer) 0
127.0.0.1:6379> get mybitmap
"ab"
127.0.0.1:6379> del mybitmap
(integer) 1
127.0.0.1:6379> set mybitmap ab
OK
127.0.0.1:6379> strlen mybitmap
(integer) 2
127.0.0.1:6379> bitcount mybitmap
(integer) 6
127.0.0.1:6379> getbit mybitmap 1
(integer) 1
127.0.0.1:6379> getbit mybitmap 2
(integer) 1
127.0.0.1:6379> getbit mybitmap 7
(integer) 1
127.0.0.1:6379> getbit mybitmap 8
(integer) 0
127.0.0.1:6379> getbit mybitmap 9
(integer) 1
127.0.0.1:6379> getbit mybitmap 10
(integer) 1
127.0.0.1:6379> getbit mybitmap 14
(integer) 1

2. bitcount

bitcount key [start end]

可用版本: 2.6.0以后

复杂度: O(N)

计算字符串中设置为1的位数(总体计数)。

默认情况下,会检查字符串中包含的所有字节。只能在传递附加参数start和end的间隔中指定计数操作。

与GETRANGE命令一样,start和end可以包含负值,以便对从字符串末尾开始的字节进行索引,其中-1是最后一个字节,-2是倒数第二个字节,依此类推。以上面的字符串“ab”为例

127.0.0.1:6379> bitcount mybitmap 0 0   #第1个字节中包含设置为1的位数
(integer) 3
127.0.0.1:6379> bitcount mybitmap 0 1   #第1-2个字节中包含设置为1的位数
(integer) 6

3. getbit

GETBIT key offset

可用版本: 2.2.0以后

复杂度: O(1)

获取offset对应的bit值,还是以上面的字符串“ab”为例

127.0.0.1:6379> getbit mybitmap 1
(integer) 1

4. bitop

BITOP <AND | OR | XOR | NOT> destkey key [key ...]

可用版本: 2.6.0以后

复杂度: O(N)

在多个键(包含字符串值)之间执行逐位操作,并将结果存储在目标键中。

BITOP命令支持四种位操作:AND、OR、XOR和NOT,因此调用该命令的有效形式为:

  • BITOP AND destkey srckey1 srckey2 srckey3 ... srckeyN #与运算
  • BITOP OR destkey srckey1 srckey2 srckey3 ... srckeyN #或运算
  • BITOP XOR destkey srckey1 srckey2 srckey3 ... srckeyN #异或运算 1 ^ 1 = 0,0 ^ 0 = 0 ,1 ^ 0 = 1
  • BITOP NOT destkey srckey #取反

注意:当在具有不同长度的字符串之间执行操作时,所有比集合中最长字符串短的字符串都被视为零填充到最长字符串的长度。对于不存在的键也是如此,它们被视为零字节流,直到最长字符串的长度。

5. bitpos

bitpos key bit [start] [end]

可用版本: 2.8.7以后

复杂度: O(N)

返回字符串中设置为1或0的第一个位的位置。

应用场景

1. 签到统计

根据业务情况可以设置多个bitmap

按天统计公司员工签到情况

# 设置当天每个员工的签到情况
setbit mark:20230316 {userCode} 1
# 查询员工是否签到
getbit mark:20230316 {userCode}
# 查看当天谁第一个签到
bitpos mark:20230316 1
# 统计当天有多少人签到
bitcount mark:20230316
# 统计连续5天都签到的人数,多个bitmap按与操作,得到一个新的bitmap
bitop and mark:20230315-20230329 mark:20230315 mark:20230316 mark:20230317 mark:20230318 mark:20230319
# 新的bitmap中统计bit位为1的就是连续签到的人数
bitcount mark:20230315-20230329 

按员工维度统计签到情况

# 设置每个员工每个月的签到情况
setbit mark:1001:202303 {day} 1
# 查询员工某一个是否签到
getbit mark:1001:202303 {day}
# 查询员工这个月签到了几次
bitcount mark:1001:202303
# 查询员工从哪天开始签到的
bitpos mark:1001:202303 1
# 查询员工从哪个开始没签到
bitpos mark:1001:202303 0

2.用户登录统计

与签到类似

bitmap存在的问题

  1. 假如就一个用户参与某个业务,而且用户ID还很大,使用setbit命令时,一下就分配了很大的内存,甚至上百兆,白白浪费了内存。所以尽量在用户ID在一定范围内分布比较均匀的时候使用bitmap。
  2. 所有的数据都存在一个key上,如果数据量比较大的话,可能会阻塞其他查询。

可以对key进行拆分,例如每个key存储M个bit,定位到属于哪个key的公式:offset/M,定位到key中哪个offset的公式:offset%M,这样既能解决大key问题,集群模式的话也能解决数据均衡存储。

例如offset=1678957598

127.0.0.1:6379> setbit mybitmap 1678957598 1
(integer) 0
127.0.0.1:6379> strlen mybitmap
(integer) 209869700                 #直接用一个key分配了200多兆内存

假如每个key存80000bit即10KB,那定位到key为:1678957598 / 80000 = 20986

对应key中的offset为:1678957598 % 80000 = 77598,所以只分配10k内存就可以

最近发表
标签列表