《Redis深度历险》四(位图)
位图
基本用法
redis的位数组是自动扩展的,如果设置了某个偏移位置而超出了现有的内容范围,就会自动将位数组进行零扩充。
零存整取例子:
先用python观察每个字符的ascii码
>>> bin(ord('h'))
'0b1101000' # 0-7位
>>> bin(ord('e'))
'0b1100101' # 8-15位
用redis设置字符的位数组
127.0.0.1:6379> setbit s 1 1
(integer) 0
127.0.0.1:6379> setbit s 2 1
(integer) 0
127.0.0.1:6379> setbit s 4 1
(integer) 0
127.0.0.1:6379> setbit s 9 1
(integer) 0
127.0.0.1:6379> setbit s 10 1
(integer) 0
127.0.0.1:6379> setbit s 13 1
(integer) 0
127.0.0.1:6379> setbit s 15 1
(integer) 0
127.0.0.1:6379> get s
"he"
零存零取例子
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
同理,可以设置字符串来查看位图,如果是不可打印字符,redis会显示该字符的十六进制形式
127.0.0.1:6379> setbit s 0 1
(integer) 0
127.0.0.1:6379> setbit s 1 1
(integer) 1
127.0.0.1:6379> get s
"\xe8e"
bitcount和bitpos
bitcount命令用来统计指定位置范围内1的个数,bitpos用来查找指定范围内出现的第一个0或1。
bitcount key [start end]
bitpos s bit(0或1)
bitcount可以用来统计用户签到次数。
bitfield
有符号数是位数组中第一个数,1为负数,0为正数,有符号最多取到64位,无符号63位
127.0.0.1:6379> bitfield s get i1 1
1) (integer) -1 #1
127.0.0.1:6379> bitfield s get i2 1
1) (integer) -1 #11
127.0.0.1:6379> bitfield s get i3 1
1) (integer) -2 #110
127.0.0.1:6379> bitfield s get i4 1
1) (integer) -3 #1101
bitfield s set u8 8 97 #从第9位开始接下来的8个位用无符号数97替换
incrby
用来对指定范围的位进行自增操作,可能会出现向下或向上溢出。redis默认的处理是折返:如果出现溢出,就将溢出的符号为丢掉,如果是8位无符号为255,加1溢出就会变为0。
bitfield w incrby u4 2 1 # 从第三位开始,对接下来的4位无符号数+1
用户可以选择溢出后行为,默认是折返wrap,还可以选择fail(报错不执行),以及饱和截断(sat)(超过了范围就停留在最大或最小值)
bitfield w overflow sat incrby u4 2 1 # 溢出后保持最大值
bitfield w overflow fail incrby u4 2 1 # 溢出后不执行
HyperLogLog
提供不精确的去重技术方案。相较于set,如果数据量上千万,可以节省不少存储空间。
pfadd 和 pfcount
分别用来增加计数和获取计数
pfadd codehole user1
1
pfcount codehole
1
pfadd codehole user2
1
pfcount codehoe user2
2
- pf是Hyperloglog的数据结构发明人名称缩写
pfmerge
用于将多个pf计数值累加在一起形成一个新的pf值
应用场景:每个页面用HyperLogLog统计uv,把所有的加起来。
注意
HyperLogLog需要占用12k的存储空间,不适合统计单个用户相关数据,如果用户有上亿个,成本比较划算。
取 Java
语言来说,一般long
占用8字节,而一字节有8位,即:1 byte = 8 bit,即long
数据类型最大可以表示的数是:2^63-1
。对应上面的2^64
个数,假设此时有2^63-1
这么多个数,从 0 ~ 2^63-1
,按照long
以及1k = 1024字节
的规则来计算内存总数,就是:((2^63-1) * 8/1024)K
,这是很庞大的一个数,存储空间远远超过12K
。而 HyperLogLog
却可以用 12K
就能统计完。
在他的实现中,有16384个桶,即 2^14 = 16384,每个桶有6位,每个桶最大表示的数字是63(0xb111111)
数据在存入时,会被hash成64位,前14位用来分桶,假设一个字符前14位是00....0010,十进制为2,那么index转为2就分到2号桶,剩下50位,最极端的情况是出现1的位置是第50位,此时index=50,转为2进制是110010。因为16384 个桶中,每个桶是 6 bit 组成的。刚好 110010 就被设置到了第 2 号桶中去了。请注意,50 已经是最坏的情况,且它都被容纳进去了。那么其他的不用想也肯定能被容纳进去。
因为fpadd的key可以设置多个value
pfadd lgh golang
pfadd lgh python
pfadd lgh java
根据上面,不同的value会被设置到不同的桶中,如果出现在同一个桶中,即前14位一样,但是后面的1位置不一样,比较原来的index是否比新index大,是则替换,否,不变。
最终,一个key对应的16384个桶,都设置了很多value,每个桶有一个k_max,此时调用pfcount,可以算出key的设置了多少次value,也就是统计值。
value被转位64位比特串,最终分配到每个桶中,所以HyperLogLog用 16384*6(后面50位用bit表示最多占用6位)/8(8位1字节)/1024 k的空间可以存储2^64个数。
之所以出现添加100万个数,统计的时候小于100万,是因为里面有写数,hash后前14位相同,分到同一个桶,后50位出现1的位置index转为2进制,如果新的大则替换,否则不变。最终每个桶都有k_max,此时便可以估算key的设置了多少次key也就是统计值。(伯努利实验,概率论相关知识。tips:一个硬币抛的次数与正面朝上的统计)
布隆过滤器
可以把布隆过滤器理解成为一个不怎么精确的set(可以判断是否存在),当使用contains方法判断某个对象是否存在时,可能会有误判。
布隆过滤器对于已经见过的元素不会误判,只会误判没见过的元素。
在使用bf.add
的时候,redis提供了自定义参数的布隆过滤器,需要在add指令之前,使用bf.reserve指令显示创建,如果对应的key存在,bf.serve会报错。bf.reserve有三个参数key、error_rate(错误率)、initial_size
error_rate越低,需要的空间越大,initial_size表示预计放入的元素数量,当实际数量超过这个数值,误判率会上升。
默认error_rate是0.01,initial_size是100.
注意事项
initial_size设置过大,会浪费存储空间,小的话会影响准确率。
大概原理
每个布隆过滤器对应到redis的数据结构是一个大型的位数组和几个不一样的无偏hash函数,所谓无偏就是能够把元素的hash值算的比较均匀,让元素能被hash映射到位数组的位置比较随机。
向布隆过滤器添加key时,会使用多个hash函数对key进行hash,算的一个整数索引值,对位数组长度进行取模运算得到一个位置,每个hash函数都会得到不同的位置,再把位数组的几个位置都置为1,就完成add操作。
向过滤器询问key是否存在,跟add一样,会把hash的几个位置都算出来,看看这几个位置是否为1,只要有一个位位0,则不存在。只有极小可能,本该为0的位,被别的key算为1,导致出错。所以数组越大越稀疏,错误概率越小。