「进击Redis」十四、Redis Bitmaps 你会了吗

2,350 阅读7分钟

前言

Redis 系列第十四篇,关于Bitmaps。我想这个玩意好哥哥们平时应该很少运用到吧,但是有一个东西叫做布隆过滤器有一种实现方式就可以基于RedisBitmaps。不熟悉布隆过滤器的好哥哥可以看下我发布的第二篇布隆过滤器这一篇就够了。看完之后好哥哥们就知道是个什么原理了,就像标题一样,一篇就够了(不是标题党)。你看看,这不是又学到了吗。无形之中装逼的力量喷涌而来。
装逼

概述

按字面意思来拆分一下bitmap。首先bit比特是二进制单位( binary unit)或二进制数字(binary digit)的缩写,是电脑记忆体中最小的单位,在二进位电脑系统中,每一bit 可以代表01(也就是二进制) 的数位讯号(非黑即白,非 0 即 1)。
map这个的话好哥哥们很熟悉了吧,像Java(这不会不知道,世界上最好的语言,没有之一)中的HashMap
Redis中,Bitmaps本身不是一种数据结构,可以把Bitmaps想象成一个以为单位的数组,数组的每个单元只能存储01,数组的下标在Bitmaps中叫做偏移量。实际上它就是字符串,但是它可以对字符串的位进行操作。

二进制表示字符串

好哥哥就会问了,那要怎么用bit来表示一个字符串呢? 那我就勉为其难的举个栗子,用bit 来表示Dawn。首先Dawn对应字母的ASCII码分别是6897119110。那bit又只能存01,所以我们把对应的ASCII码转换成二进制得到01000100011000010111011101101110(一个字节Byte占八位bit)。如下图 Dawn

ASCII码表(这么贴心的给好哥哥们把码表都准备好了,确定不点赞加关注吗) ASCII

Bitmaps 存储结构

上面我们已经知道Bitmaps大概的一些概念,那在 Redis 中它是个什么样子的呢?实际上是差不多的,把对应的二进制字符串存入当成 value,这样是不是就可以对字符串进行做位进制的计算了,如图
bitmaps

命令

背景:将每个独立用户是否访问过网站存放在Bitmaps中,将访问的用户 记做1,没有访问的用户记做0,用偏移量作为用户的 id。

设值

设置键的第offset个位的值(从 0 算起),假设现在有 20 个用户, userid=0,5,11,15,19的用户对网站进行了访问,那么当前 Bitmaps 初始化结果如下图。
bitmaps

## 格式 key:键, offset:下标偏移量, value: 0或1
setbit key offset value
## 具体的操作过程
127.0.0.1:6379> setbit unique:users:2020-12-13 0 1
(integer) 0
127.0.0.1:6379> setbit unique:users:2020-12-13 5 1
(integer) 0
127.0.0.1:6379> setbit unique:users:2020-12-13 11 1
(integer) 0
127.0.0.1:6379> setbit unique:users:2020-12-13 15 1
(integer) 0
127.0.0.1:6379> setbit unique:users:2020-12-13 19 1
(integer) 0

如果此时有一个userid=50的用户访问了网站,那么 Bitmaps 的结构变成如下图,第 20 位~49 位都是 0。 50
平时我们很多应用的用户id以一个指定数字(例如 10000 或者使用雪花算法产生)开头,直接将用户idBitmaps的偏移量对应势必会造成一定的浪费,通常的做法是每次做setbit操作时将用户 id 减去这个指定数字。在第一次初始化 Bitmaps 时,假如偏移量非常大,那么整个初始化过程执行会比较慢,可能会造成 Redis 的阻塞。

取值

如果我们需要获取用户 id 为 11 在 2020-12-13 是否有访问过网站,就可以使用以下的命令

## 格式 key:键, offset:下标偏移量
gitbit key offset
## 获取结果就是0或者1,如果offset在key中不存在则返回0
127.0.0.1:6379> getbit unique:users:2020-12-13 11
(integer) 1

获取 Bitmaps 指定范围内值为 1 的个数

按天统计访问过网站的用户数量

127.0.0.1:6379> bitcount unique:users:2020-12-13
(integer) 5

如果要计算用户 id 在第 1 个字节(一个字节等于 8 位,0-17offset)到第 3 个字节之间的独立访问用户数,对应的用户 id 是 11,15,19。

## 格式 [start]和[end]代表起始和结束字节数
bitcount [start][end]
## 返回统计结果
127.0.0.1:6379> bitcount unique:users:2020-12-13 1 3
(integer) 3

Bitmaps 间的运算

bitop是一个复合操作,它可以做多个Bitmapsand(交集)、or(并集)、not(非)、xor(异或)操作并将结果保存在destkey中。
举个栗子,假设 2020-12-12 访问网站的 userid 为 1、2、5、9。
交集: 计算出 2020-12-12 和 2020-12-13 两天都访问过网站的用户数量

## 格式 op: 具体操作(and or),destkey: 结果集
bitop op destkey key[key....]
## 取交集
127.0.0.1:6379> bitop and unique:users:and:2020-12-12_13 unique:users:2020-12-12
unique:users:2020-12-13
(integer) 2
127.0.0.1:6379> bitcount unique:users:and:2020-12-12_13
(integer) 2

并集: 算出 2020-12-12 和 2020-12-13 任意一天都访问过网站的用户数量(例如月活跃就是类似这种)

## 取并集
127.0.0.1:6379> bitop or unique:users:or:2020-12-12_13 unique:users:2020-12-12 unique:users:2020-12-13
(integer) 2
127.0.0.1:6379> bitcount unique:users:or:2020-12-12_13
(integer) 6

计算 Bitmaps 中第一个值为 targetBit 的偏移量

例如我们要计算在 2020-12-13 在第 0 个字节到第 1 个字节之间访问网站的最小用户 id

## 格式 targetBit: 0或者1,[start]和[end]代表起始和结束字节数,和上面一样
bitpos key targetBit [start] [end]
## 栗子
127.0.0.1:6379> bitpos unique:users:2020-12-13 0 0 1
(integer) 0

对比

大数据量和高日活量场景下。假设网站有 1 亿用户,每天独立访问的用户有 5 千万,如果每天用集合类型和 Bitmaps 分别存储活跃用户结果图
日活
好哥哥,看到区别了吗。很明显,这种情况下使用 Bitmaps 能节省很多的内存空间,尤其是随着时间推移节省的内存还是非常可观的
统计
但是如果说这个网站的用户量很少,假设只有 10 万,那么使用 Bitmaps(基数太大)就不是很合适了,大量的值都是 0。 基数

使用场景

  1. 统计网站的日/月活量
  2. 用户按天签到
  3. 统计用户登录、在线等状态
  4. 实现布隆过滤器,不熟悉的可以看布隆过滤器这一篇就够了 (后面会有实现 Demo)

总结

合理使用Bitmaps是能极大的减少内存空间的占用,在某些只有两个状态的统计中,性能也是非常好的。但是在值存储状态上来说Bitmaps的值实际上只能存bit,也就是说只能存 0 或者 1。这也就意味着我们需要统计或者标记的状态不能超过两个,例如男或者女,当某种神秘力量出现第三种其他性别时就无法完成统计或者标记。这也是使用Bitmaps的一个局限性。
另外,如果使用不当,会出现浪费内存空间、效率低下的情况,上面已经举过栗子了。好哥哥们酌情使用哦。

本期就到这啦,有不对的地方欢迎好哥哥们评论区留言,另外求关注、求点赞 下一篇:奇妙的 Redis HyperLogLog
上一篇:Redis 万字长文 Lua 详解