场景引入
我们在正常开发环境中,有时候需要将bool型数据进行存取,比如用户一年里面签到了多少次,签到了设置1,没签到设置0,要记录365天,如果使用普通的key/value形式存储,每个用户就需要占据365键值对,当用户量上亿的时候,需要惊人的存储空间。更何况是一年的。 为了解决这种问题,redis提出了bitmap的数据结构,这样每天用户签到只需要占据一个位,365天就是365位,46个字节,一个稍微长一点的字符串就可以完全容纳下一个用户一年的签到记录,大量的节省存储空间。位图的最小单位是比特(bit),每个bit的取值只能是0或1。
实现原理
位图不是特殊的数据结构,他的内容实际就是普通的字符串,也就是byte数组,我们可以使用普通的get/set直接获取和设置整个位图的内容,也可以使用位图操作getbit/setbit等将byte数组看成位数组来处理。
基本用法
redis的位数组是自动扩展的,如果设置了某个偏移位置超出了现有的内容范围,就会自动将位数组进行零扩充。
举例:
“h”的ASCII码值是:01101000
"e"的ASCII码值是: 01100101
"l"的ASCII码值是:0110 1100
"o"的ASCII码值是:0110 1111
将“he” 连起来是:0110100001100101
即1,2,4,9,10,13,15位为1
以上的示范可以称之为“零存整取”,即使用单个位操作设置位值,使用单个位操作获取具体位值。
还有另一种操作称之为“整存零取”,即使用字符串操作批量设置值,使用单个位操作获取具体位值。
以上介绍了setbit,getbit的操作,redis还提供了位图的统计和查找指令:bitcount,bitpos
bitcount同来统计指定位值范围内1的个数。
bitpos用来查找指定范围内出现的第一个0或者1。
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
127.0.0.1:6379>
接下来介绍魔术指令 bitfield:
主要解决setbit/getbit只能操作单个位的弊端。redis 3.2+新增功能。
bitfield有三个子指令:get、set、incrby,他们都可以对指定位片段进行读写,但是最多只能处理64个连续的位,如果超过64位,就得使用多个子指令,bitfield可以一次执行多个子指令。
127.0.0.1:6379> set w hello
OK
127.0.0.1:6379> bitfield w get u4 0 //从第一个位开始取4个位,结果是无符号数(u)
1) (integer) 6
127.0.0.1:6379> bitfield w get u3 2 //从第三个位开始取3个位,结果是无符号数
1) (integer) 5
127.0.0.1:6379> bitfield w get i4 0 //从第一个位开始取4个位,结果是有符号数 (i)
1) (integer) 6
127.0.0.1:6379> bitfield w get i3 2 //从第三个位开始取3个位,结果是有符号数
1) (integer) -3
127.0.0.1:6379>
所谓有符号数是指获取的位数组中第一个位是符号位,剩下的才是值,如果第一个位是1,那就是负数。
无符号数表示非负数,没有符号位,获取的位数全部是是值。
有符号数最多可以获取64位,无符号数只能获取63位。如果超出限制,redis会报参数错误。
接下来演示一个多指令:
127.0.0.1:6379> bitfield w get u4 0 get u3 2 get i4 0 get i3 2
1) (integer) 6
2) (integer) 5
3) (integer) 6
4) (integer) -3
127.0.0.1:6379>
接下来使用set子指令将第二个字符e,改成a,a的ASCII值是97
127.0.0.1:6379> bitfield w set u8 8 97
1) (integer) 101
127.0.0.1:6379> get w
"hallo"
127.0.0.1:6379>
接下来介绍第三个子指令incrby,他用来对指定范围的位进行自增操作,既然是自增操作,就会存在溢出的情况,如果增加了正数,会出现向上溢出,如果是增加了负数,就会出现向下溢出。redis的默认处理方式是折返操作,如果出现了溢出,就将溢出的符号位丢掉。如果是8位无符号数255,加1后就会溢出,会全部变为0.如果是8位有符号数127,加1后就会溢出变成-128。
127.0.0.1:6379> set w hello
OK
127.0.0.1:6379> bitfield w incrby u4 2 1 //从第三个位开始,对接下来的4位无符号数进行自增+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 //出现了溢出折返现象
1) (integer) 0
127.0.0.1:6379>
bitfield指令提供了溢出策略子指令overflow,用户可以选择溢出行为,默认是折返(wrap),还可以选择失败(fail)------报错不执行,以及饱和截断(sat)-----超过了范围就停留在最大值或者最小值。overflow指令只影响接下来的第一条指令,这条指令执行完后溢出策略会变成默认值折返。
饱和截断:
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>
失败不执行:
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>