位图
位图,即大量bit组成的一个数据结构(每个bit只能是0和1),主要适合在一些场景下,进行空间的节省,并有意义的记录数据,
例如一些大量的bool类型的存取,一个用户365天的签到记录,签到了是1,没签到是0,如果用普通的key/value进行存储,当用户量很大的时候,需要的存储空间是很大的。
如果使用位图进行存储,一年365天,用365个bit就可以存储,365个bit换算成46个字节(一个稍长的字符串),如此就节省了很多的存储空间,
位图的本质其实是一个普通的字符串,也就是byte数组,可以使用get/set直接获取和设置整个位图的内容,也可以使用 getbit/setbit 将byte数组看成bit数组来处理。
使用位操作设置字符串
正常设置字符串都使用set命令,下面我们使用setbit设置一下位数组,最后以获取字符串的形式获取,
首先我们获取一下h、e两个ASCII码使用二进制的表示如下,
可以看到h的二进制码是 01101000 , e的二进制码是 01100101,我们只需要注意bit是1的位置,然后进行setbit,
需要注意的是,位数组的顺序和字符的位顺序是反的,根据这个原则,我们算出 h字符 每个1的位置分别是1/2/4, e字符的则是 9/10/13/15,
所以我们将使用setbit设置一个位数组,并在每个位置上(1/2/4/9/10/13/15)设置对应的1,
setbit data 1 1
setbit data 2 1
setbit data 4 1
setbit data 9 1
setbit data 10 1
setbit data 13 1
setbit data 15 1
零存整取
最后直接 get data这个key,会发现正好得到he,
setbit + get 的组合称为 零存整取,零存就是一个bit一个bit的设置,整取就是通过key名字,直接get出来所有的数据,
同样,我们还可以进行 零存零取,整存零取,整存就是直接使用字符串设置整个位数组,零取则是通过bit的位置,进行bit的获取。
零存零取
可以看到,我们根据setbit,对key叫做w的位数组进行bit设置,只设置了1/2/4这3个位置的值为1,下图中有getbit w 3, 获取第三个位置的值,此时默认是0,如果从业务角度触发,可以理解为,一共签到4天,第三天没有进行签到,
整存零取
下午所示,我们对w的这个key,直接set了一个h字符,随后通过getbit获取w的位数组里的每个bit,可以看到获取出来的内容和上面h字符的二进制内容相同 1/2/4的位置是1,其余是0
注意
- redis的位数组是自动扩充的,如果设置的某个偏移位置超出了现有的内容范围,就会自动将位数组进行零扩充,即扩容的位默认都是0值。
- 如果对应位的字节是不可打印字符,redis-cli将会显示该字符的十六进制形式。
- 一个字节是8个bit(位),要区分字节和位。
统计和查找 (bitcount/bitpos)
redis提供了 统计指令 bitcount 和 位图查找指令 bitpos ,
bitcount用来统计指定位置范围内1的个数,bitpos用来查找指定范围内出现的第一个0或1。
我们可以通过bitcount统计用户一共签到了多少天,通过bitpos指令查找用户从哪一天开始第一次签到,
如果指定了范围参数[start, end],就可以统计在某个时间范围内用户签到了多少天以及用户自某天以后的哪天开始签到,
但是需要注意的是,start和end参数是字节索引,也就是说,指定的位范围必须是8的倍数,
而不能任意指定,所以我们无法直接计算某个月内用户签到了多少天,如果需要计算的话,
可以使用getrange命令取出该月覆盖的字节内容,然后在内存中进行统计,例如2月覆盖了10-12个字节,就使用 getrange w 8 12 。
127.0.0.1:6379> set w hello
OK
127.0.0.1:6379> bitcount w # 所有字符中有多少个1
(integer) 21
127.0.0.1:6379> bitcount w 0 0 # 第一个字符中 1 的位数
(integer) 3
127.0.0.1:6379> bitcount w 0 1 # 前两个字符中 1 的位数
(integer) 7
127.0.0.1:6379> bitpos w 0 # 第一个 0 位
(integer) 0
127.0.0.1:6379> bitpos w 1 # 第一个 1 位
(integer) 1
127.0.0.1:6379> bitpos w 1 1 1 # 从第二个字符算起,第一个1位
(integer) 9
127.0.0.1:6379> bitpos w 1 2 2 # 从第三个字符算起,第一个1位
(integer) 17
bitfield
之前介绍的 setbit / getbit 指定位的值都是单个位,如果要一次操作多个位,就必须使用管道来处理,
在redis3.2以后,提供了bitfield指令,可以一次对多个位进行操作,bitfield有三个子指令,分别是get/set/incrby, 都可以对指定位片段进行读写,
但是最多只能处理64个连续的位,如果超过64位,就需要使用多个子指令,bitfield可以一次执行多个子指令。
示例
下面对下图的位数组使用 bitfield 做一些操作
127.0.0.1:6379> bitfield w get u4 0 # 从第1个位开始取4个位,结果是无符号数(u)
1) (integer) 6
127.0.0.1:6379> bitfield w get u3 2 # 从第3个位开始取3个位,结果是无符号数(u)
1) (integer) 5
127.0.0.1:6379> bitfield w get i4 0 # 从第1个位开始取4个位,结果是有符号数(i)
1) (integer) 6
127.0.0.1:6379> bitfield w get i3 2 # 从第3个位开始取3个位,结果是有符号数(i)
1) (integer) -3
有符号数是指获取的位数组中的第一个位是符号位,剩下的才是值,如果第一位是1,就是负数,
无符号数表示非负数,没有符号位,获取的位数组全部都是值,有符号数最多可以获取64位,
无符号数只能获取63位,因为redis协议中的integer是有符号数,最大64位,不能传递64位的无符号值,
如果超出位数限制,redis就会告诉你参数错误。
上面的指令可以合并成一条指令,可以看到得到的结果是一样的,
bitfield w get u4 0 get u3 2 get i4 0 get i3 2
set修改
我们从第9个位开始,用8个无符号数替换已经存在的8个位,其实就是把第二个字符替换了,由e变成a(它的ASCII码是97),可以看到结果也变成了 hallo
127.0.0.1:6379> bitfield w set u8 8 97
1) (integer) 101
127.0.0.1:6379> get w
"hallo"
incrby
incrby对指定范围的位进行自增操作,即++,这可能会发生溢出,如果增加了正数,会出现上溢出,如果增加的是负数,会出现下溢出,
redis默认的处理是折返,即如果出现了溢出,就将溢出的符号位丢掉,例如,如果是8位无符号数255,加1后就全部变成0,如果是8位有符号数127,加1后就溢出变成-128。
依然根据hello字符,来演示一下 incrby
127.0.0.1:6379> set w hello
OK
127.0.0.1:6379> bitfield w get u4 2 # 从第3位开始取4个无符号整数,第一次是10
1) (integer) 10
127.0.0.1:6379> bitfield w incrby u4 2 1
1) (integer) 11
127.0.0.1:6379> bitfield w incrby u4 2 1
1) (integer) 12
127.0.0.1:6379> bitfield w incrby u4 2 1
1) (integer) 13
127.0.0.1:6379> bitfield w incrby u4 2 1
1) (integer) 14
127.0.0.1:6379> bitfield w incrby u4 2 1
1) (integer) 15
127.0.0.1:6379> bitfield w incrby u4 2 1 #到这里的时候,已经溢出折返成0了
1) (integer) 0
bitfield指令提供溢出策略子指令 overflow,用户可以选择溢出行为,默认是折返(wrap),还可以选择失败(fail) 即报错不执行,还有饱和截断(sat) 即超过范围就停留在最大或最小值,
overflow指令只影响接下来的第一条指令,这条指令执行完以后,溢出策略就会变成默认值 折返(wrap)。
饱和截断
127.0.0.1:6379> set w hello
OK
127.0.0.1:6379> bitfield w overflow sat incrby u4 2 1
1) (integer) 11
127.0.0.1:6379> bitfield w overflow sat incrby u4 2 1
1) (integer) 12
127.0.0.1:6379> bitfield w overflow sat incrby u4 2 1
1) (integer) 13
127.0.0.1:6379> bitfield w overflow sat incrby u4 2 1
1) (integer) 14
127.0.0.1:6379> bitfield w overflow sat incrby u4 2 1
1) (integer) 15
127.0.0.1:6379> bitfield w overflow sat incrby u4 2 1 # 接下来的都将是保持最大值
1) (integer) 15
127.0.0.1:6379> bitfield w overflow sat incrby u4 2 1
1) (integer) 15
127.0.0.1:6379> bitfield w overflow sat incrby u4 2 1
1) (integer) 15
失败不执行
127.0.0.1:6379> set w hello
OK
127.0.0.1:6379> bitfield w overflow fail incrby u4 2 1
1) (integer) 11
127.0.0.1:6379> bitfield w overflow fail incrby u4 2 1
1) (integer) 12
127.0.0.1:6379> bitfield w overflow fail incrby u4 2 1
1) (integer) 13
127.0.0.1:6379> bitfield w overflow fail incrby u4 2 1
1) (integer) 14
127.0.0.1:6379> bitfield w overflow fail incrby u4 2 1
1) (integer) 15
127.0.0.1:6379> bitfield w overflow fail incrby u4 2 1 # 接下来的都是失败
1) (nil)
127.0.0.1:6379> bitfield w overflow fail incrby u4 2 1
1) (nil)
127.0.0.1:6379> bitfield w overflow fail incrby u4 2 1
1) (nil)
get/set/incrby一起执行
127.0.0.1:6379> bitfield w set u4 1 0 get u4 1 incrby u4 2 1
1) (integer) 0
2) (integer) 0
3) (integer) 1
127.0.0.1:6379> get w
"\x04ello"