Redis之bitmap———节衣缩食

一、bitmap介绍

bitmap,也叫位图,是一种实现对位的操作的'数据结构',用一个bit位来表示一个东西的状态,我们都知道bit位是二进制,所以只有两种状态,0或1。

  • bitmap同样属于redis的string数据类型,也就是byte数组,Redis中一个字符串类型的值最多能存储512M的内容,每个字符由多个字节组成,一个字节由8个bit位构成,所以bitmap最多可以存储2^32位
  • 虽然底层位string,但是redis为bitmap提供了一套单独的命令,所以用法区别与字符串,我们可以讲位图看成是数组,里面全是0和1表示的内容(二进制位数组),数组的下标在bitmap中叫做offest(偏移量)

我们在开发中,如果需要bool类型的存取,比如用户一年的签到记录,用户每天做没做核酸,每个用户一年按365天存储,就是365条数据,如果上亿个用户来存储这样的数据,需要的存储空间是吓人的,bitmap的出现就是为了大数据量,但是前提是需要存储的内容只能由两种状态,因为每个bit只能表示两种状态,只能为0或1,比如我们想实现一个100万个用户一年(365天)每天的核酸情况只需要 365/8 * 100000 / 1024 / 1024 = 43.86M的空间

二、操作命令

1、setbit key offset value

给key值在指定索引位置上(offest)设置一个value,因为bit位所以value只能为0和1,否则会出现错误(error) ERR bit is not an integer or out of range,offest的范围为0-2^32,返回值为set之前的值,默认全部为0

2、getbit key offset

获取key在指定位置上的值,返回值为0或者1,如果未set,则默认为0

3、bitcount key [start end]

统计key在指定范围内的1的出现次数,0, -1或者不加参数表示统计全部

4、bitbops key bit [start end]

统计key中指定范围内的bit(0或1)第一次出现的索引位置,没有则返回-1

5、bittop operation destkey key [key ...]

将两个或者多个key的二进制位进行或、与的操作然后将结果赋值给新的key(destkey)

6、魔术指令bitfield

语法

BITFIELD key [GET type offset] [SET type offset value] [INCRBY type offset increment] [OVERFLOW WRAP|SAT|FAIL]


前面我们设置(setbit)和获取(getbit)都是针对单个位操作的,如果需要一次性指定或者获取多个位,就必须使用管道来进行操作,redis在3.2.0以后新增了bitfield命令可以一次性多多个位进行处理

  • offset参数跟之前的一样,用于设置获取获取指定的起始偏移量,偏移量为0表示从位图的第1个二进制开始
  • type参数用于指定被设置值的类型,这个参数需要以i或u开头,后跟被设置值的位数,i表示被设置值为有符号整数,u表示被设置值为无符号整数,比如i8表示设置值为有符号8位整数,u16表示被设置值为无符号16位整数
  • value表示被设置的整数值,这个值的类型应该和type参数指定的类型一致,如果给定的值超过了type参数类型指定的值,那么set 命令根据type参数指定的类型进行截断,比如如果将123(二进制位为001101100)存储到u4类型上,那么set命令会截取位4位1011(十进制的11 )然后进行设置。


set命令返回指定区域被设置之前的值


我们可以尝试用set子命令将第二个字符e改成a,a的ASCII码为97,

bitfield bit set u8 8 97表示从第8个位开始,将接下来的8位用无符号数97替换

redis的位数组是自动扩展,如果设置的某个偏移量超出了当前现有的内容范围,就会自动将位数组进行零扩充,默认为0
因为底层是字符串,所以我们可以用strlen命令查看一下bitmap存储所占用的长度,
strlen命令返回的是字符串所占用的长度,因为一个数字占用一个字节,而bit这个key只set偏移量为0的值,未开辟新的内存空间,所以strlen返回值为1

因为一个bit占8个字节,所以我们在索引位置为8的地方添加一个值

可以看到新开了一个字节空间,所以返回值为2
因为位图是直接操作字符串的二进制位,所以我们通过设置bit位来将字符串设置为“liu”(不是通过set命令)
首先我们将字符串转为字符数组,再将字符数组转为二进制位
String str = "liu"; char[] chars = str.toCharArray(); for (char aChar : chars) { System.out.println(Integer.toBinaryString(aChar)); }
可以输出为1101100 1101001 1110101,从低位到高位,位数组的顺序和字符的顺序是相反的,所以字符串“liu”的二进制为01101100 01101001 01110101
下面我们用redis的bit来设置,可以看到第一个字符l的1/2/4/5位为1,字符i的1/2/4/7为1,字符u的1/2/3/5/7位为1


三、getbit理解

上面我们了解了到bitmap的占用空间小,还有一个特点是执行效率高,上述命令getbit key offest在0-2^32范围内获取到的bit时间复杂度竟然为O(1)
假如我们要将0-2^32个数(0或1)中的1进行统计,我们会怎么统计,最直接暴力的可能是直接循环依次相加

private static int customBitCount (int num) {
		String s = Integer.toBinaryString(num);
		char[] chars = s.toCharArray();
		int count = 0;
		for (char aChar : chars) {
			if ((aChar & 1) == 1) {
				count++;
			}
		}
		return count;
	}

当很大时,这样显然是行不通的,上面我们了解了,bitcount返回指定范围内1出现的次数,即bitcount要解决的问题:统计一个位数组中非0二进制位的数量,在数学上被称为汉明重量(Hamming Weight),目前已知效率最搞的算法是variable-precision SWAR,而redis的bitcount也用的是SWAR算法,
我们可以看到redis源码src/bitops.c文件,可以看到bitCount的命令


/* BITCOUNT key [start end [BIT|BYTE]] */
void bitcountCommand(client *c) {
    robj *o;
    long long start, end;
    long strlen;
    unsigned char *p;
    char llbuf[LONG_STR_SIZE];
    int isbit = 0;
    unsigned char first_byte_neg_mask = 0, last_byte_neg_mask = 0;

    /* Lookup, check for type, and return 0 for non existing keys. */
    if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.czero)) == NULL ||
        checkType(c,o,OBJ_STRING)) return;
    p = getObjectReadOnlyString(o,&strlen,llbuf);

    /* Parse start/end range if any. */
    if (c->argc == 4 || c->argc == 5) {
        long long totlen = strlen;
        /* Make sure we will not overflow */
        serverAssert(totlen <= LLONG_MAX >> 3);
        if (getLongLongFromObjectOrReply(c,c->argv[2],&start,NULL) != C_OK)
            return;
        if (getLongLongFromObjectOrReply(c,c->argv[3],&end,NULL) != C_OK)
            return;
        /* Convert negative indexes */
        if (start < 0 && end < 0 && start > end) {
            addReply(c,shared.czero);
            return;
        }
        if (c->argc == 5) {
            if (!strcasecmp(c->argv[4]->ptr,"bit")) isbit = 1;
            else if (!strcasecmp(c->argv[4]->ptr,"byte")) isbit = 0;
            else {
                addReplyErrorObject(c,shared.syntaxerr);
                return;
            }
        }
        if (isbit) totlen <<= 3;
        if (start < 0) start = totlen+start;
        if (end < 0) end = totlen+end;
        if (start < 0) start = 0;
        if (end < 0) end = 0;
        if (end >= totlen) end = totlen-1;
        if (isbit && start <= end) {
            /* Before converting bit offset to byte offset, create negative masks
             * for the edges. */
            first_byte_neg_mask = ~((1<<(8-(start&7)))-1) & 0xFF;
            last_byte_neg_mask = (1<<(7-(end&7)))-1;
            start >>= 3;
            end >>= 3;
        }
    } else if (c->argc == 2) {
        /* The whole string. */
        start = 0;
        end = strlen-1;
    } else {
        /* Syntax error. */
        addReplyErrorObject(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 {
        long bytes = (long)(end-start+1);
        long long count = redisPopcount(p+start,bytes);
        if (first_byte_neg_mask != 0 || last_byte_neg_mask != 0) {
            unsigned char firstlast[2] = {0, 0};
            /* We may count bits of first byte and last byte which are out of
            * range. So we need to subtract them. Here we use a trick. We set
            * bits in the range to zero. So these bit will not be excluded. */
            if (first_byte_neg_mask != 0) firstlast[0] = p[start] & first_byte_neg_mask;
            if (last_byte_neg_mask != 0) firstlast[1] = p[end] & last_byte_neg_mask;
            count -= redisPopcount(firstlast,2);
        }
        addReplyLongLong(c,count);
    }
}

在统计我们可以看到是在redisPopcount方法中

/* 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 an input string length up to 512 MB or more (server.proto_max_bulk_len) */
long long redisPopcount(void *s, long count) {
    long long bits = 0;
    unsigned char *p = s;
    uint32_t *p4;
    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. */
    while((unsigned long)p & 3 && count) {
        bits += bitsinbyte[*p++];
        count--;
    }

    /* Count bits 28 bytes at a time */
    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;

        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;
    }
    /* Count the remaining bytes. */
    p = (unsigned char*)p4;
    while(count--) bits += bitsinbyte[*p++];
    return bits;
}

参考文档
https://zhuanlan.zhihu.com/p/480386998
《Redis深度历险-核心原理与应用实践》应用3

posted @ 2022-09-27 18:23  木马不是马  阅读(311)  评论(0编辑  收藏  举报