浅谈Redis位图(Bitmap)及Redis二进制中的问题
作者:殘荷听雨
Redis位图(Bitmap)及二进制的问题
SETBIT key offset value
对 key 所储存的字符串值,设置或清除指定偏移量上的位(bit)。位的设置或清除取决于 value 参数,可以是 0 也可以是 1 。当 key 不存在时,自动生成一个新的字符串值。字符串会进行伸展(grown)以确保它可以将 value 保存在指定的偏移量上。当字符串值进行伸展时,空白位置以 0 填充。offset 参数必须大于或等于 0 ,小于 2^32 (bit 映射被限制在 512 MB 之内)。返回值是指定偏移量原来存储的位
对使用大的 offset 的 SETBIT 操作来说,内存分配可能造成 Redis 服务器被阻塞。具体参考 SETRANGE 命令,warning(警告)部分。
127.0.0.1:6379> setbit bit 3 1 (integer) 0 127.0.0.1:6379> getbit bit 0 (integer) 0 127.0.0.1:6379> getbit bit 1 (integer) 0 127.0.0.1:6379> getbit bit 2 (integer) 0 127.0.0.1:6379> getbit bit 3 (integer) 1127.0.0.1:6379> setbit bit 3 0(integer) 1
GETBIT key offset
返回key对应的string在offset处的bit值,当offset超出了字符串长度的时候,这个字符串就被假定为由0比特填充的连续空间。当key不存在的时候,它就认为是一个空字符串,所以offset总是超出范围,然后value也被认为是由0比特填充的连续空间。到内存分配。
127.0.0.1:6379> getbit yhq 5 (integer) 0 127.0.0.1:6379> getbit bit 10 (integer) 0 127.0.0.1:6379> getbit bit 3 (integer) 1
BITCOUNT key [start] [end]
计算给定字符串中,被设置为 1 的比特位的数量。一般情况下,给定的整个字符串都会被进行计数,通过指定额外的 start 或 end 参数,可以让计数只在特定的位上进行*start和end的单元是字节而不是bit*。start 和 end 参数的设置和 GETRANGE 命令类似,都可以使用负数值:比如 -1 表示最后一个位,而 -2 表示倒数第二个位,以此类推。不存在的 key 被当成是空字符串来处理,因此对一个不存在的 key 进行 BITCOUNT 操作,结果为 0 。
127.0.0.1:6379> set mykey foobar OK 127.0.0.1:6379> bitcount youkey (integer) 0 127.0.0.1:6379> bitcount mykey (integer) 26 127.0.0.1:6379> bitcount mykey 0 0 # "f" 0110 0110 (integer) 4 127.0.0.1:6379> bitcount mykey 1 1 # "o" 0110 1111 (integer) 6
BITOP operation destkey key [key ...]
对一个或多个保存二进制位的字符串 key 进行位元操作,并将结果保存到 destkey 上。
operation 可以是 AND 、 OR 、 NOT 、 XOR 这四种操作中的任意一种:
- BITOP AND destkey key [key ...] ,对一个或多个 key 求逻辑与,并将结果保存到 destkey 。
- BITOP OR destkey key [key ...] ,对一个或多个 key 求逻辑或,并将结果保存到 destkey 。
- BITOP XOR destkey key [key ...] ,对一个或多个 key 求逻辑异或,并将结果保存到 destkey 。
- BITOP NOT destkey key ,对给定 key 求逻辑非,并将结果保存到 destkey 。
除了 NOT 操作之外,其他操作都可以接受一个或多个 key 作为输入。
处理不同长度的字符串,当 BITOP 处理不同长度的字符串时,较短的那个字符串所缺少的部分会被看作 0 。空的 key 也被看作是包含 0 的字符串序列
127.0.0.1:6379> setbit bit1 0 1 (integer) 0 127.0.0.1:6379> setbit bit1 3 1 (integer) 0 127.0.0.1:6379> setbit bit2 0 1 (integer) 0 127.0.0.1:6379> setbit bit2 1 1 (integer) 0 127.0.0.1:6379> setbit bit2 3 1 (integer) 0 127.0.0.1:6379> bitop and andbit bit1 bit2 (integer) 1 127.0.0.1:6379> getbit andbit 0 (integer) 1 127.0.0.1:6379> getbit andbit 1 (integer) 0 127.0.0.1:6379> getbit andbit 2 (integer) 0 127.0.0.1:6379> getbit andbit 3 (integer) 1
BITPOS key bit [start] [end]
返回字符串里面第一个被设置为1或者0的bit位。如果我们在空字符串或者0字节的字符串里面查找bit为1的内容,那么结果将返回-1。
如果我们在字符串里面查找bit为0而且字符串只包含1的值时,将返回字符串最右边的第一个空位。如果有一个字符串是三个字节的值为0xff的字符串,那么命令BITPOS key 0将会返回24,因为0-23位都是1。基本上,我们可以把字符串看成右边有无数个0。然而,如果你用指定start和end范围进行查找指定值时,如果该范围内没有对应值,结果将返回-1。
127.0.0.1:6379> getbit num 0 (integer) 0 127.0.0.1:6379> getbit num 1 (integer) 0 127.0.0.1:6379> getbit num 2 (integer) 1 127.0.0.1:6379> getbit num 3 (integer) 1 127.0.0.1:6379> getbit num 4 (integer) 0 127.0.0.1:6379> getbit num 5 (integer) 0 127.0.0.1:6379> getbit num 6 (integer) 1 127.0.0.1:6379> getbit num 7 (integer) 0 127.0.0.1:6379> getbit num 8 (integer) 0 127.0.0.1:6379> bitpos num 1 (integer) 2 127.0.0.1:6379> bitpos yhqqhh 1 (integer) -1 127.0.0.1:6379> bitpos yhqqhh 0 (integer) 0
Redis二进制中的问题1 : 数字全部是char类型表示
127.0.0.1:6379> set num 2 OK 127.0.0.1:6379> bitcount num (integer) 3
Redis中,数字类型其实是以ASCII形式展现的,即 2=>50,正常2的(一个字节8个二进制位)表示为 00000010,bitcount为1。而Redis数字是字符的"2",所以 "2"[50] 的表示为 00110010,bitcount为3。
Redis中二进制从左到右(正常从右到左)
"2"的二进制为 00110010
127.0.0.1:6379> getbit num 0 (integer) 0 127.0.0.1:6379> getbit num 1 (integer) 0 127.0.0.1:6379> getbit num 2 (integer) 1 127.0.0.1:6379> getbit num 3 (integer) 1 127.0.0.1:6379> getbit num 4 (integer) 0 127.0.0.1:6379> getbit num 5 (integer) 0 127.0.0.1:6379> getbit num 6 (integer) 1 127.0.0.1:6379> getbit num 7 (integer) 0
redis高级数据结构---bitmap
场景引入
我们在正常开发环境中,有时候需要将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>
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。