【Redis源码】位图SETBIT、BITCOUNT(九)

20 篇文章 3 订阅

前言:

 

位图并不是一个特殊的数据结构,位图其实就是一个字符串,位图这种结构占用空间特别小。如果是数亿以上的用户如果存储活跃,如果是用key/value去存储每个节点都需要数G去存储,存储是很大问题。如果换做位图去存储则可以大大节约空间,不过适应于用户ID连续性的。除此之外也可以用作比如说点赞的存储。

 

redis版本:4.0.0

 

(一)命令解析

命令原形

命令

备注

setbit key offset value

setbit name 0 1

设置name键值偏移位置0的值为1

getbit key offset

gebit name 0

获取name键值偏移位置0的值

bitcount key [start end]

bitcount name

统计name键值为1的总数

setbit存储解析:

 

1)上图中操作setbit bitstr 0 1和setbit bitstr 7 1其实只占用1个字节,创建sds创建byte+1多加了一个字节。所以看到是2个字节。

2) 上图中操作setbit bitstr 25 1此时的byte等于4个字节,多加一个。所以是5个字节。

3) byte计算形式等于 offset / 8 。这个byte其实是计算存储到那个数组字节中。1个字节=8bit

4)计算存储在哪个bit里, (offset % 8 ) + 1。

 

如图:

(二)setbit源码解析

 

setbit命令,bitops.c中:

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;

    if (getBitOffsetFromArgument(c,c->argv[2],&bitoffset,0,0) != C_OK)   //获得offset偏移,字符串有一个512MB的限制
        return;

    if (getLongFromObjectOrReply(c,c->argv[3],&on,err) != C_OK)        //获得值
        return;

    /* 当前值只能是1和0 */
    if (on & ~1) {
        addReplyError(c,err);
        return;
    }

    if ((o = lookupStringForBitCommand(c,bitoffset)) == NULL) return;   //查找或者创建字符串sds或扩容

    /* Get current values */
    byte = bitoffset >> 3;    //btye = bitoffset / 8 计算字节位置
    byteval = ((uint8_t*)o->ptr)[byte];  //获得存储到字节
    
    /*
    假设bitoffset=25。当前(1 << (7 - (bitoffset & 0x7)))= 64
    bit = 64对应二进制 = 0100 0000 
    bit用于做位运算
    */
    bit = 7 - (bitoffset & 0x7);          
    bitval = byteval & (1 << bit);       
    
    /* Update byte with new bit value and return original value */
    byteval &= ~(1 << bit);
    byteval |= ((on & 0x1) << bit);
    ((uint8_t*)o->ptr)[byte] = byteval;
    signalModifiedKey(c->db,c->argv[1]);
    notifyKeyspaceEvent(NOTIFY_STRING,"setbit",c->argv[1],c->db->id);
    server.dirty++;
    addReply(c, bitval ? shared.cone : shared.czero);
}

获得offset偏移

int getBitOffsetFromArgument(client *c, robj *o, size_t *offset, int hash, int bits) {
    long long loffset;
    char *err = "bit offset is not an integer or out of range";
    char *p = o->ptr;
    size_t plen = sdslen(p);
    int usehash = 0;

    /* Handle #<offset> form. */
    if (p[0] == '#' && hash && bits > 0) usehash = 1;

    if (string2ll(p+usehash,plen-usehash,&loffset) == 0) {
        addReplyError(c,err);
        return C_ERR;
    }

    /* Adjust the offset by 'bits' for #<offset> form. */
    if (usehash) loffset *= bits;

    /* 512MB字符串限制,不能超过 */
    if ((loffset < 0) || ((unsigned long long)loffset >> 3) >= (512*1024*1024))
    {
        addReplyError(c,err);
        return C_ERR;
    }

    *offset = (size_t)loffset;
    return C_OK;
}

查找或者创建字符串sds或扩容函数,bitops.c中:

robj *lookupStringForBitCommand(client *c, size_t maxbit) {
    size_t byte = maxbit >> 3;   //btye = bitoffset / 8 计算字节位置
    robj *o = lookupKeyWrite(c->db,c->argv[1]);

    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); //获得robj对象
        o->ptr = sdsgrowzero(o->ptr,byte+1);  //内部使用了sdsMakeRoomFor重新计算sds字符串长度
    }
    return o;
}

 

(三)理解variable-precision SWAR算法

 

统计一个位数组中非0位的数量,数学上称作:”Hanmming Weight“(汉明重量)。最高的是variable-precision SWAR算法,可以在常数时间内计算出多个字节的非0数目。

十六进制

二进制

备注

0x55555555

0101 0101 0101 0101 0101 0101 0101 0101

bit的奇数为1,偶数为0

0x33333333

0011 0011 0011 0011 0011 0011 0011 0011

每两位二进制为两位1

0x0F0F0F0F

0000 1111 0000 1111 0000 1111 0000 1111

每四位二进制为四位1

0x01010101

0000 0001 0000 0001 0000 0001 0000 0001

每1个字节最后一位都是1

 

swar函数:

int swar(uint32_t i)
{
    //计算每2位二进制数中1的个数
    i = ( i & 0x55555555) + ((i >> 1) & 0x55555555);
    //计算每4位二进制数中1的个数
    i = (i & 0x33333333) + ((i >> 2) & 0x33333333);
    //计算每8位二进制数中1的个数
    i = (i & 0x0F0F0F0F) + ((i >> 4) & 0x0F0F0F0F);
    //将每8位二进制数中1的个数和相加,并移至最低位8位
    i = (i * 0x01010101) >> 24;
    return i;
}

1)第一次计算

 

通过 ( i & 0x55555555) + ((i >> 1) & 0x55555555)计算后所得0x51618294,( i & 0x55555555) 得到每两位奇数的1的个数,而((i >> 1) & 0x55555555)在每两位上得到偶数的1个数。二者相加得到的就是每两位上1的个数。

每两位的取值范围二进制: 01 、10、11。其实就是1、2、3。

 

2)第二次计算

 

通过 (i & 0x33333333) + ((i >> 2) & 0x33333333)计算后所得0x21312231。计算每4位的1的个数。

 

 

3)第三次计算

 

通过 (i & 0x0F0F0F0F) + ((i >> 2) & 0x0F0F0F0F)计算后所得0x3040404。

计算每8位的1的个数。

 

 

4)第四次计算

 

0x3040404 * 0x0101010计算所得其实是一个long类型的数字0x3070b0f0c0804。但是由于uint32_t只占四位。多余部分会被移除所以只剩下0x0f0c0804,而0x0f0c0804对应的二进制是00001111 00001100 00001000 00000100 。右移动24位之后剩下00001111,而00001111对应的就是15。

 

 

(四)bitcount源码解析

 

bitcount命令

void bitcountCommand(client *c) {
    robj *o;
    long start, end, strlen;
    unsigned char *p;
    char llbuf[LONG_STR_SIZE];

    。。。 省略
    
    /* 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 {
        long bytes = end-start+1;

        addReplyLongLong(c,redisPopcount(p+start,bytes)); //统计字节数组中的1数量
    }
}

统计字节数组中的1数量函数

size_t redisPopcount(void *s, long count) {
    size_t bits = 0;
    unsigned char *p = s;
    uint32_t *p4;
    /**
    预先生成对应的bit表,因为一个字节非负数是可以是0~255个数字。所以生成了一个256数组。
    比如说0对应的二进制一个1都没有,bitsinbyte[0]则为0
    255对应二进制全是1,bitsinbyte[255]则为8
    */
    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};

    /* 计算对齐,以4字节对齐。多余部分先计算到bits中 */
    while((unsigned long)p & 3 && count) {
        bits += bitsinbyte[*p++];
        count--;  //减少统计字节数
    }

    /* 
    开始用variable-precision SWAR算法计算“1”的个数,
    计算以 28 字节宽度统计,节约计算时间
     */
    p4 = (uint32_t*)p;
    while(count>=28) { //统计字节 >= 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;

        aux1 = aux1 - ((aux1 >> 1) & 0x55555555);  //第一步
        aux1 = (aux1 & 0x33333333) + ((aux1 >> 2) & 0x33333333); //第二步
        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);
        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; //第三步
    }
    
    /* 计算剩余部分字节 */
    p = (unsigned char*)p4;
    while(count--) bits += bitsinbyte[*p++];
    return bits;
}

 

总结:

 

1) 位图适合解决大数据存储减少存储资源。

2) setbit是通过sds字符串存储,是按照位存储。

3)bitcount采用SWAR算法,其实还采用了bitsinbyte预生成数组去减少计算开销。

4)setbit的最大限制是512MB

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值