文章目录
- 本人 github 地址
- 前言
- 跟bitmap相关的命令
- setbit源码分析
- bitcount源码分析
- 怎样求一个二进制串里面有多少个1
- bitmap使用场景
- 总结
本人 github 地址
github 地址 里面有注释好的代码,下载下来可以方便阅读。
前言
由于这周工作关系,没有太多事件阅读太多源码,但是项目常常提到一些用到bitmap的场景,这次我们来把redis 的bit操作命令一网打尽
跟bitmap相关的命令
setbit: setbit 顾名思义就是将一个用字符串表示的二进制,在它的某一位上面设置0或者1.
完整命令为 setbit key offset value
key 为必填参数 ,即键值空间里面的键,不管什么命令都是通过键值空间去搜索的。
offset 也是必填参数,offset是一个正整数,表示二进制串的某一位,offset 有限制要小于512*1024*1024*8
value 也是必填参数,表示某一位设置为0或者1. value只能为0或者1
返回结果: 返回这一位上面原来的那个值。
实例:
setbit abc 1 0
(integer) 0
getbit: 和setbit 是对应关系,主要用于获取二进制串某一位上面的value
完整命令为 getbit key offset
因为和setbit的参数相似的,则不单独说明
实例:
getbit abc 1
(integer) 0
bitop: bitop 主要对于多个对应key的二进制串,做and,or,xor, not操作。
完整命令为 bitop operation destkey key [key]
operation ,为必填项,有以下4个操作,and, or, xor, not , 忽略大小写
destkey , 为必填项,目标存储key,目标存储key是做完上面操作需要存到键值空间里面的可以和后面的key无关
key, 1个到多个,如果只有一个的时候,直接将key的value存到destkey里面去。
返回参数:为新key的长度(字节数),等于合并的key最大长度的key。(这里说的长度是key对应value的长度)
实例:
bitop and abc3 abc abc1
(integer) 1
bitcount: bitcount 是用来统计某一个二进制串里面1的个数
完整命令为 bitcount key [start end]
start 和 end 为可选项,这里的start 和end 是起始字节数。
返回参数:二进制串里面的1的个数
实例
bitcount abc 1 8
(integer) 0
上面这句话的意思统计 key为abc对应的字符串上面 从第一个字节开始到第8个字节结束,有多少个1. 一个string abc 应该看作一个byte[]数组,这样就很好理解了
bitpos: bitpos 是用来查询一个二进制串里第一个0或者1的位置。
完整命令为 bitpos key bit [start end]
bit , 为必填项,取值范围为0或者1.
start 和 end 为可选项,这里的start 和end 是起始字节数。
返回参数:二进制串第一个0或者1的位置
实例
bitpos abc 1 1 1
(integer) 13
bitfield: bitfield 主要针对一个二进制字符串做位宽操作
完整命令为 BITFIELD key [GET type offset] [SET type offset value] [INCRBY type offset increment] [OVERFLOW WRAP|SAT|FAIL]
命令详见: link.
其中 type,是用来指定位宽是一个用什么类型的参数来表达,如 u64,i63, u代表无符号整数,i代表有符号整数,当为u时取值范围为1-64,当为i的时候1-63,不满8位的会自动补齐
下面我们挑两个命令来分析其源码第一个为setbit, 第二个为bitcount
setbit源码分析
首先来看到命令的定义:
{"setbit",setbitCommand,4,
"write use-memory @bitmap",
0,NULL,1,1,1,0,0,0},
然后看到对应处理命令方法
/* SETBIT key offset bitvalue */
// 设置位图
void setbitCommand(client *c) {
robj *o;
char *err = "bit is not an integer or out of range";
size_t bitoffset;
ssize_t byte, bit;
int byteval, bitval;
long on;
//将参数赋值到bitoffset
if (getBitOffsetFromArgument(c,c->argv[2],&bitoffset,0,0) != C_OK)
return;
//将参数赋值到on
if (getLongFromObjectOrReply(c,c->argv[3],&on,err) != C_OK)
return;
/* Bits can only be set or cleared... */
//on 只能0或者1
if (on & ~1) {
addReplyError(c,err);
return;
}
//获取对应的string
if ((o = lookupStringForBitCommand(c,bitoffset)) == NULL) return;
/* Get current values */
//将bitoffset 转变成byteoffset
//方便找到bitoffset 对应的byteoffset
byte = bitoffset >> 3;
//直接那一位的byte 提取出来
byteval = ((uint8_t*)o->ptr)[byte];
//比如bitoffset 是13,那么 13&7,决定了他在那个byte是第几位
//但是我们用string 表示二进制的时候,是从左至右
//而表示整数的时候从右至左所以要用7去减
bit = 7 - (bitoffset & 0x7);
//获得那一位是0或者1.
bitval = byteval & (1 << bit);
/* Update byte with new bit value and return original value */
//将byteval 上面对应的那一位变成0
byteval &= ~(1 << bit);
//更新新的值在那一位上面
byteval |= ((on & 0x1) << bit);
//赋值到string里面
((uint8_t*)o->ptr)[byte] = byteval;
signalModifiedKey(c,c->db,c->argv[1]);
//事件通知,比如像通知到monitor 命令这种
notifyKeyspaceEvent(NOTIFY_STRING,"setbit",c->argv[1],c->db->id);
//每次change的时候都会加1.
server.dirty++;
addReply(c, bitval ? shared.cone : shared.czero);
}
/* This is an helper function for commands implementations that need to write
* bits to a string object. The command creates or pad with zeroes the string
* so that the 'maxbit' bit can be addressed. The object is finally
* returned. Otherwise if the key holds a wrong type NULL is returned and
* an error is sent to the client. */
robj *lookupStringForBitCommand(client *c, size_t maxbit) {
//这个是字节数,多少位/8等于字节数
size_t byte = maxbit >> 3;
//查询key
robj *o = lookupKeyWrite(c->db,c->argv[1]);
//如果o为null
if (o == NULL) {
//则创建一个比他大一个字节的字符串
o = createObject(OBJ_STRING,sdsnewlen(NULL, byte+1));
//加入到字典
dbAdd(c->db,c->argv[1],o);
} else {
if (checkType(c,o,OBJ_STRING)) return NULL;
o = dbUnshareStringValue(c->db,c->argv[1],o);
o->ptr = sdsgrowzero(o->ptr,byte+1);
}
return o;
}
下图省略了sds 的头部结构
上面有两个知识点,
- setbit的value类型也是string,只是用string表达一个二进制串
- 在做设置位的时候,因为string是从左到右,当我们转换位二进制的时候是从右到左开始读的,所以去求position 的时候需要反过来。
bitcount源码分析
bitcount 命令的定义
// 获取某个二进制串上面1的个数
{"bitcount",bitcountCommand,-2,
"read-only @bitmap",
0,NULL,1,1,1,0,0,0},
bitcount的主要处理入口:
/* BITCOUNT key [start end] */
void bitcountCommand(client *c) {
robj *o;
long start, end, strlen;
unsigned char *p;
char llbuf[LONG_STR_SIZE];
/* Lookup, check for type, and return 0 for non existing keys. */
//查询对应的key 是否存在
if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.czero)) == NULL ||
checkType(c,o,OBJ_STRING)) return;
//赋值string 到p
p = getObjectReadOnlyString(o,&strlen,llbuf);
/* Parse start/end range if any. */
if (c->argc == 4) {
//转化为start
if (getLongFromObjectOrReply(c,c->argv[2],&start,NULL) != C_OK)
return;
//转化为end
if (getLongFromObjectOrReply(c,c->argv[3],&end,NULL) != C_OK)
return;
/* Convert negative indexes */
//检验start 和end是否合法
if (start < 0 && end < 0 && start > end) {
addReply(c,shared.czero);
return;
}
//如果start 小于0 则,用strlen 去减
if (start < 0) start = strlen+start;
if (end < 0) end = strlen+end;
if (start < 0) start = 0;
if (end < 0) end = 0;
if (end >= strlen) end = strlen-1;
} else if (c->argc == 2) {
//如果没有start 和end 则是0到strlen-1
/* The whole string. */
start = 0;
end = strlen-1;
} else {
/* Syntax error. */
addReply(c,shared.syntaxerr);
return;
}
/* Precondition: end >= 0 && end < strlen, so the only condition where
* zero can be returned is: start > end. */
if (start > end) {
addReply(c,shared.czero);
} else {
//所要计算的bytes数
long bytes = end-start+1;
addReplyLongLong(c,redisPopcount(p+start,bytes));
}
}
这个bitcount的核心方法是redisPopcount,用来计算一段二进制数的1的个数,在分析代码前,我们先进入下面这个主题。
怎样求一个二进制串里面有多少个1
用到下面这个算法,必须为2的次方,如果不为2的次方的话可以通过下面redis这种方式来处理
首先我们举一个8位的数,如 11011000。我们可以用肉眼方式知道结果为4,那怎么用o(logn)的时间复杂度统计出来,
第一阶段:
首先我们可以聚焦到相邻两位上面,只能出现4种组合,11,10,01,00,但是如果用两位来表示有多少个1,只可能出现3种组合,10,01,00 ,这三种情况,如果用写代码逻辑来表达,相邻两位1的个数=数值本身or数值本身-1, 那什么时候需要-1了,就是当第二位为的1的时候。
设第二位为x,第一位为y, xy>>1&01=0x.
然后xy-0x,就等于相邻两位的次数了。x只可能为0或者1.
放到8位里面就变成了 11011000>>1&01010101; 结果为01000100
然后11011000-01000100,结果10010100,
第二阶段:
现在相邻两位的次数已经得到,我们继续,每两位之间相加。我们看到前4位是1001,我们的目的就是让高两位要放到低两位来相加,那么就有了第一步1001>>2,但是考虑到这是我们的一个分段思考所以还得1001>>2&0011.
所以10010100 ,10010100>>2&00110011+10010100&00110011,结果为00110001。
第三阶段:
第三次我们就要每4位之间相加了,00110001>>4&00001111+00110001&00001111结果为00000100。
下图代码为求8位数的有多少个1的代码
unsigned int popcount(uint8_t value){
//0x55 等于01010101
//第一阶段
value = value-((value>>1)&0x55);
//第二阶段
//0x33 等于00110011
value =(value&0x33)+((value>>2)&0x33);
//第三阶段
//0xf 等于00001111
value =(value&0x0f)+((value>>4)&0x0f);
return value;
}
看完上面这个例子我们再来看redis的是怎么做的
/* -----------------------------------------------------------------------------
* Helpers and low level bit functions.
* -------------------------------------------------------------------------- */
/* Count number of bits set in the binary array pointed by 's' and long
* 'count' bytes. The implementation of this function is required to
* work with a input string length up to 512 MB. */
size_t redisPopcount(void *s, long count) {
size_t bits = 0;
unsigned char *p = s;
uint32_t *p4;
//这个意思就是一个字节的上面0-255 上面分别有多少个1
static const unsigned char bitsinbyte[256] = {0,1,1,2,1,2,2,3,1,2,2,3,2,3,3,4,1,2,2,3,2,3,3,4,2,3,3,4,3,4,4,5,1,2,2,3,2,3,3,4,2,3,3,4,3,4,4,5,2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,1,2,2,3,2,3,3,4,2,3,3,4,3,4,4,5,2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,3,4,4,5,4,5,5,6,4,5,5,6,5,6,6,7,1,2,2,3,2,3,3,4,2,3,3,4,3,4,4,5,2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,3,4,4,5,4,5,5,6,4,5,5,6,5,6,6,7,2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,3,4,4,5,4,5,5,6,4,5,5,6,5,6,6,7,3,4,4,5,4,5,5,6,4,5,5,6,5,6,6,7,4,5,5,6,5,6,6,7,5,6,6,7,6,7,7,8};
/* Count initial bytes not aligned to 32 bit. */
//因为p是指针,因为下面要一次要计算28个字节的count
//所以当p的地址非4整除的地址,先处理掉余数位
//这里还考虑到了cpu,每次选址过程都是2的倍数原理
//不然可能需要两个cpu的指令周期
while((unsigned long)p & 3 && count) {
bits += bitsinbyte[*p++];
count--;
}
/* Count bits 28 bytes at a time */
//每次处理28位
p4 = (uint32_t*)p;
while(count>=28) {
uint32_t aux1, aux2, aux3, aux4, aux5, aux6, aux7;
aux1 = *p4++;
aux2 = *p4++;
aux3 = *p4++;
aux4 = *p4++;
aux5 = *p4++;
aux6 = *p4++;
aux7 = *p4++;
count -= 28;
//0x55555555=101010.......
aux1 = aux1 - ((aux1 >> 1) & 0x55555555);
aux1 = (aux1 & 0x33333333) + ((aux1 >> 2) & 0x33333333);
//0x33333333=00110011.......
aux2 = aux2 - ((aux2 >> 1) & 0x55555555);
aux2 = (aux2 & 0x33333333) + ((aux2 >> 2) & 0x33333333);
aux3 = aux3 - ((aux3 >> 1) & 0x55555555);
aux3 = (aux3 & 0x33333333) + ((aux3 >> 2) & 0x33333333);
aux4 = aux4 - ((aux4 >> 1) & 0x55555555);
aux4 = (aux4 & 0x33333333) + ((aux4 >> 2) & 0x33333333);
aux5 = aux5 - ((aux5 >> 1) & 0x55555555);
aux5 = (aux5 & 0x33333333) + ((aux5 >> 2) & 0x33333333);
aux6 = aux6 - ((aux6 >> 1) & 0x55555555);
aux6 = (aux6 & 0x33333333) + ((aux6 >> 2) & 0x33333333);
aux7 = aux7 - ((aux7 >> 1) & 0x55555555);
aux7 = (aux7 & 0x33333333) + ((aux7 >> 2) & 0x33333333);
//0x0
bits += ((((aux1 + (aux1 >> 4)) & 0x0F0F0F0F) +
((aux2 + (aux2 >> 4)) & 0x0F0F0F0F) +
((aux3 + (aux3 >> 4)) & 0x0F0F0F0F) +
((aux4 + (aux4 >> 4)) & 0x0F0F0F0F) +
((aux5 + (aux5 >> 4)) & 0x0F0F0F0F) +
((aux6 + (aux6 >> 4)) & 0x0F0F0F0F) +
((aux7 + (aux7 >> 4)) & 0x0F0F0F0F))* 0x01010101) >> 24;
}
/* Count the remaining bytes. */
//剩下不满28位的放到这里来处理。
p = (unsigned char*)p4;
while(count--) bits += bitsinbyte[*p++];
return bits;
}
如果前面的8位的例子你已经看懂的话那么那么只有最后一步不懂了
好的我们再来分析最后一步是什么意思((aux1…)*0x01010101)>>24
假定 aux已经算出每8位1的个数了
那么aux*0x01010101 又等价于aux*0x01000000+aux*0x00010000+aux*0x00000100+aux*0x00000001
又等价于
aux<<24+aux<<16+aux<<8+aux,这样的目的是把,后面低24位都放到,第一个高8位,又因为一个32位的数,最多能有32个1,那么只要不是8个32位数一起累加,就不可能发生(字节)越位(即低8位的计算进位到高8位)的现象,所以这里redis为什么选择28字节一起来计算也是因为这一点。
经过上面的分析
最后一个>>24 也不用再过多说明了。
bitmap使用场景
bitmap 使用场景太多了最常见的就是黑名单和白名单,适合id唯一且自增的整数,对于一些用id字符长串可以通过一些hash的手段来结合一起去用,
bitmap 本质上还是一个string串,当offset 太大也会产生内存copy等性能问题,还有对于一些数据比较希疏的场景,也会极大的浪费内存,不推荐使用bitmap的手段,还是可以用字典类型去代替
总结
这章最主要的学习如何运用位运算达到我们的处理结果的目的,同时我们也学习redis bitmap相关的命令。但是我们可以注意到其实bitmap相关命令最终存储的value 还是string 类型,所以一些set 相关的命令也可能会造成对于bit命令的破坏,当然我们也可以通过存储set命令直接存储一个二进制串这些也是可以的。