文章目录

  • 本人 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 的头部结构

bitmap redis 菜鸟 redis bitmap命令_redis


上面有两个知识点,

  1. setbit的value类型也是string,只是用string表达一个二进制串
  2. 在做设置位的时候,因为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。

bitmap redis 菜鸟 redis bitmap命令_bit_02

下图代码为求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命令直接存储一个二进制串这些也是可以的。