标题:【Redis07】Redis基础:Bitmap 与 HyperLogLog 相关操作
Redis基础学习:Bitmap 与 HyperLogLog 相关操作
继续进行 Redis 基础部分的学习,今天我们学习的是两种另外的数据类型。说是数据类型,但其实它们实际上使用的都是 String 类型做为底层基础,只不过是在存储的时候进行了一些特殊的操作。换句话说,这两种类型并不是真正意义上的“数据类型”,换成“数据操作”可能更合适一些。
Bitmap
先来看看 Bitmap ,这是个啥玩意?其实呀,这个就是 位图 ,Bit 就是 比特 的意思嘛。一提到 比特 是不是又得来二进制了?这个真没办法,学计算机的,任何工具都逃不掉的就是 二进制 相关的操作。不过在 Redis 中的操作还是比较简单的,毕竟我们主要的操作还是在程序业务端进行,Redis 只是一个数据库而已。
Bitmap 在 Redis 中可以存储大约 40 多亿条数据,或者说是40多亿个 0 和 1 位。估计不少小伙伴马上就能想到它的应用场景了,我们在最后再说。先来看看它的操作。
Bitmap 操作命令
设置和获取指定位置的位信息,直接使用 SETBIT 和 GETBIT。
127.0.0.1:6379> setbit a 8 1
(integer) 0
127.0.0.1:6379> setbit a 4 1
(integer) 0
127.0.0.1:6379> get a
"\b\x80"
127.0.0.1:6379> getbit a 4
(integer) 1
127.0.0.1:6379> getbit a 5
(integer) 0
注意到上面的例子,我们直接使用 GET 命令也可以获取到内容,前面就说过了,它本身就是使用 String 类型存储的。只不过使用位操作的话,我们要理解 8 位代表 1 个字节的问题。因此,设置一个 8 位的二进制代码才能表示出一个我们能看懂的字符。
127.0.0.1:6379> setbit b 1 1
(integer) 0
127.0.0.1:6379> setbit b 4 1
(integer) 0
127.0.0.1:6379> setbit b 5 1
(integer) 0
127.0.0.1:6379> setbit b 6 1
(integer) 0
127.0.0.1:6379> setbit b 7 1
(integer) 0
127.0.0.1:6379> get b
"O"
127.0.0.1:6379> setbit c 1 1
(integer) 0
127.0.0.1:6379> setbit c 2 1
(integer) 0
127.0.0.1:6379> setbit c 4 1
(integer) 0
127.0.0.1:6379> setbit c 6 1
(integer) 0
127.0.0.1:6379> setbit c 7 1
(integer) 0
127.0.0.1:6379> get c
"k"
看出上面的例子是什么意思了吗?第一个 b ,设置的其实是 01001111 ,转换成十进制是 79 ,对应的 ASC2 码就是大写英文 O 。另一个 c 也是一样的意思,01101011 的十进制是 107 ,表示的英文是 107 。如果要表示 Ok 这两个字母的话,就需要两个 8 位,也就是两个字节 01001111 01101011 。
127.0.0.1:6379> setbit d 1 1
(integer) 0
127.0.0.1:6379> setbit d 4 1
(integer) 0
127.0.0.1:6379> setbit d 5 1
(integer) 0
127.0.0.1:6379> setbit d 6 1
(integer) 0
127.0.0.1:6379> setbit d 7 1
(integer) 0
127.0.0.1:6379> get d
"O"
127.0.0.1:6379> setbit d 9 1
(integer) 0
127.0.0.1:6379> setbit d 10 1
(integer) 0
127.0.0.1:6379> setbit d 12 1
(integer) 0
127.0.0.1:6379> setbit d 14 1
(integer) 0
127.0.0.1:6379> setbit d 15 1
(integer) 0
127.0.0.1:6379> get d
"Ok"
好玩吧?中文也是一样的,只是三个字节的 UTF8 编码,需要三个 8 位的数据才能表示一个中文字,大家可以试试哦。
127.0.0.1:6379> set s 中
OK
127.0.0.1:6379> get s
"\xe4\xb8\xad"
127.0.0.1:6379> getbit s 0
(integer) 1
127.0.0.1:6379> getbit s 1
(integer) 1
127.0.0.1:6379> getbit s 2
(integer) 1
127.0.0.1:6379> getbit s 3
(integer) 0
“中”这个字直接 GET 返回的是16进制数据,将 e4b8ad 转换成二进制是 11100100 10111000 10101101 ,然后我们就可以使用 GETBIT 来验证是不是和我们手动转换出来的是一样的结果。
大家有兴趣的可以再多了解一下字符编码相关的知识,比如在 Redis 中使用的 UTF8 ,为什么单个字节第一位只能是 0 ,必须是 0xxxxxxx 这样的,中文也都是固定的 1110xxxx 10xxxxxx 10xxxxxx 这种格式,很有意思哦。
位操作
既然是位操作,那么 与、或、异或、非 操作肯定要有啦。
127.0.0.1:6379> bitop or dd b c
(integer) 1
127.0.0.1:6379> get dd
"o"
127.0.0.1:6379> bitop and dd b c
(integer) 1
127.0.0.1:6379> get dd
"K"
127.0.0.1:6379> bitop xor dd b c
(integer) 1
127.0.0.1:6379> get dd
"$"
127.0.0.1:6379> bitop not dd b
(integer) 1
127.0.0.1:6379> get dd
"\xb0"
查询第一个bit位位置
通过 BITPOS 命令,可以查询到指定的 key 中,第一个出现的位数据的位置。
127.0.0.1:6379> BITPOS a 1
(integer) 4
127.0.0.1:6379> BITPOS a 0
(integer) 0
第一条命令是查询第一个 1 出现的位置,第二条命令是查询第一个 0 出现的位置。它还有第二个参数,这个参数是一个偏移量,不过需要注意的是,它指的是字节的偏移量,不是位的偏移量。
127.0.0.1:6379> BITPOS d 1 0
(integer) 1
127.0.0.1:6379> BITPOS d 1 1
(integer) 9
127.0.0.1:6379> BITPOS d 1 2
(integer) -1
统计数量
BITCOUNT 可以统计指定 key 中 1 出现的次数。
127.0.0.1:6379> BITCOUNT d
(integer) 10
127.0.0.1:6379> BITCOUNT d 1 0 2
(error) ERR syntax error
127.0.0.1:6379> BITCOUNT d 1 0 1
(error) ERR syntax error
127.0.0.1:6379> BITCOUNT d 1 2
(integer) 5
127.0.0.1:6379> BITCOUNT d 0 2
(integer) 10
127.0.0.1:6379> BITCOUNT d 2 3
(integer) 0
它还有两个参数,同样也是字节偏移量和长度。
位域操作
位域这个东西我就不太懂了,只是给个例子,这一块深入学过 C 的同学应该会比较了解。
127.0.0.1:6379> setbit e 1 1
(integer) 0
127.0.0.1:6379> get e
"@"
127.0.0.1:6379> BITFIELD e incrby i5 100 1 get u4 0
1) (integer) 1
2) (integer) 4
127.0.0.1:6379> get e
"@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80"
Bitmap 经典例子
好了,Bitmap 的基本操作就是上面那些,介绍的比较简单,不过也确实都不复杂。但前提是要对二进制和进制之间的转换以及字符编码要有一定的了解。如果我们只是为了去显示字符串的位,那就有点大材小用了,其实 Bitmap 有个非常强悍的能力,也就是运用 BITCOUNT 去进行数据统计。
包括官网上给出的也是类似这样的例子,统计登录用户数。
最开始我们已经知道,一个 key 可以保存40多亿个位,那么我们可以把用户id当作位索引,然后某个用户今天登录了,就给它的位设置为 1 ,然后就可以 BITCOUNT 快速统计出今天有多少用户登录了系统,速度相当快哦。
127.0.0.1:6379> setbit user_login_20220509 100010 1
(integer) 0
127.0.0.1:6379> setbit user_login_20220509 25525 1
(integer) 0
127.0.0.1:6379> setbit user_login_20220509 8782 1
(integer) 0
127.0.0.1:6379> setbit user_login_20220509 9846526547 1
(error) ERR bit offset is not an integer or out of range
127.0.0.1:6379> setbit user_login_20220509 98465265 1
(integer) 0
127.0.0.1:6379> setbit user_login_20220509 399999999 1
(integer) 0
127.0.0.1:6379> setbit user_login_20220509 3999999999 1
(integer) 0
127.0.0.1:6379> setbit user_login_20220509 4294967295 1
(integer) 0
127.0.0.1:6379> setbit user_login_20220509 4294967296 1
(error) ERR bit offset is not an integer or out of range
127.0.0.1:6379> BITCOUNT user_login_20220509
(integer) 7
HyperLogLog
既然说到统计了,那么 HyperLogLog 这个操作类型就不得不提了。它是一种概率数据结构,用于计算唯一事物的集合数量。同时,它是以内存换精度的,也就是说,它能极大的节约内存,但是会有精度丢失的问题。最坏情况下,也就是内存占用最大的情况下,它也只需要 12K 的内存容量就可以存储非常巨大的数据量,精度的丢失会在 1% 以内。它也可以实现上面 Bitmap 中统计的例子,我们先来看看相关的操作命令。
127.0.0.1:6379> pfadd hll a b c d e f g
(integer) 1
127.0.0.1:6379> pfcount hll
(integer) 7
127.0.0.1:6379> pfadd hll a b c d e f g h i j k
(integer) 1
127.0.0.1:6379> pfcount hll
(integer) 11
PFADD 添加数据,PFCOUNT 获取数量。基本的操作命令就这两个,是不是无敌了。除了这两个外,还有一个合并的命令,类似于集合的并集操作。
127.0.0.1:6379> PFADD pfa a b c d e f
(integer) 1
127.0.0.1:6379> PFADD pfb d c a e g i
(integer) 1
127.0.0.1:6379> pfadd pfc b d f i l m n o
(integer) 1
127.0.0.1:6379> PFMERGE pfmerge pfa pfb pfc
OK
127.0.0.1:6379> pfcount pfmerge
(integer) 12
另外,它也是以 String 为基本存储类型的,所以用 GET 也能看到内容。
127.0.0.1:6379> get hll
"HYLL\x01\x00\x00\x00\x0b\x00\x00\x00\x00\x00\x00\x00Fm\x80I\xe8\x80L\"\x80D<\x848\x80B=\x80K\x83\x80B\xed\x84A\xfc\x8cG\x8e\x80Bm\x80BZ"
好了,接下来说下 HyperLogLog 和其它方式统计的对比,以下是网上找到的相关文章获取到的资料。
如果我们使用 SET 来进行基数统计,那么假设每一个元素的 32Bit(2^24 ≈ 1600万; 2^32 ≈ 42亿) ,
假设存储1亿个不重复的元素那么我们需要 100 000 000 * 32 /8/1024/1024 ≈ 381MB。
如果我们用 Bitmap 来进行基数统计,每个元素对应一位(bit),假设我们存储1亿个不重复的元素那么我们需要
100 000 000 /8/1024/1024 ≈ 12MB。
然而我们使用 HyperLogLog ,一个键占用的内容空间是12KB,并且这个键可以处理海量数据。
它们三个都可以应用在统计不重复元素的场景:
HyperLogLog:海量数据,可以忍受 0.81% 误差的场景,其实大数据处理的时候都会有误差。
Bitmap:数据量不大,不能忍受误差,元素连续的(自增的用户主键id)。
SET:数据量不大,不能忍受误差,元素没有规律,并且需要返回实际的单个元素。
总结
今天的内容挺好玩吧,Bitmap 和 HyperLogLog 最常用的其实都是一个不重复数据统计的场景,但是又各有优势。在日常的工作中如果有类似的应用场景,完全就可以使用这两种数据操作来试试了。
扩展知识:布隆过滤器
布隆过滤器(Bloom Filter),听说过没?面试有没有被坑过?跟你说,布隆过滤器的基础知识就是 Bitmap ,HyperLogLog 也是它的类似实现之一。啥叫布隆过滤器?就是能够快速地查找某一个元素是否存在于指定的集合中,最典型的做法就是使用二进制位来进行操作,这不就是 Bitmap 嘛。
当然,完整的布隆过滤器的实现还是要更复杂一些,它一般会有三个 Hash 函数,生成三个不同位置,只有当三个位置全部命中时,才会认为指定的数据已经存在。从这一点上来说,其实就是在有限的空间内可以存放更多的数据。但它也会出现不同的数据三个 Hash 函数计算结果一致的概率,但我们可以增加更多的 Hash 函数,不过相应地性能也会降低。同样,它也会有精度问题,反正大概原理就是这样。布隆过滤器有一个非常经典的名言,那就是“我说你不在,那你一定不在;我说你存在,你有可能存在(也可能不存在)!”。在 packgist 上也能搜到纯 PHP 实现布隆过滤器的 Composer 包,大家可以自己下载源码学习一下哦!
视频链接
微信文章地址:https://mp.weixin.qq.com/s/bxciIQgcN6otIowjw9ETbQ