本文转载自微信公众号《Java极客技术》,作者鸭血范。转载本文请联系Java极客技术公众号。Redis作为当代互联网行业不可替代的Key-Value数据库,在我们的日常工作中扮演着重要的角色。相信大家对常用的命令都不陌生。今天给大家分享一种可能很少用到,但又很重要的BitArray。下面先用一个简单的命令来了解命令的用法,然后再介绍底层的实现原理,帮助大家更好的理解。简单使用我们先来看看什么是BitArray位数组。Redis使用字符串对象来存储位数组。一个Byte字节有8位。通过控制每一位为0或1,代表一个元素对应的值或状态。使用8位可以为复杂的运算节省大量空间。与BitArray相关的操作命令有SETBIT、GETBIT、BITCOUNT、BITOP。下面依次看看命令的使用,最后看看实现原理。首先我们在本地启动一个Redis实例,然后启动一个客户端链接如下图,通过redis-cli连接客户端,执行相应命令,然后使用BitArray相关命令,通过setbit测试21命令我们创建了一个名为test的位数组并将其第二位设置为1,然后使用getbittest2获取对应位的值。setbit命令的作用是将对应key的指定偏移位置设置为1或0,getbit命令是获取指定偏移位置的值。test是一个位数组,通过上面的命令,其值变为00000010。接下来,我们创建一个名为test2的位数组,并多次使用setbit命令和bitcount。bitcount命令的作用是统计位数组中1的个数。通过下面我们看到第一次使用bitcounttest2命令的结果是1,当我们在使用setbittest211命令后再次使用bitcount命令时,发现结果变成了2。其中,test2的开头是00000100,然后变成00000101。bitop命令相信大家都能看懂。都是AND、OR、XOR、NOT的运算,就不细说了。具体用法见上图。原理前面说过,Redis是通过字符串对象来实现位数组的,所以字符串对象的功能在位数组上是可以实现的。Redis底层的位数组存储结构也是基于SDS(simpledynamicstrings),如下:其中len字段表示包含的buf数组个数,buf[i]表示第i个中的具体值字节数组,buf[len]是末尾的分隔符\0。上图中的buf[0]是一个字节,有8位。使用setbit命令后,初始值为00000000,buf[1]为分隔符\0。SETBIT我们在执行setbitkeyoffsetvalue命令的时候,分为两步:计算要创建多少字节数组(offset/8)+1;判断长度是否不够展开;计算offset对应的字节位置=offset/8;计算offset对应的bit,bit=(offsetmod8)+1;根据offset找到对应的位置,把这里的值改成value并返回旧值;假设我们执行命令setbittest231,第一步计算字节数(3/8)+1=1,计算后只需要一个字节;第二步,与原来的len比较,发现不需要扩容;3、根据offset计算存储如果byte3/8=0,则存储在buf[0];第四部分计算位,(3mod8)+1=4,这意味着第四位。经过一轮test2后变成00001000。setbit命令执行的操作是常量级的,时间复杂度为O(1)。GETBIT我们知道了setbit命令是如何实现的,那么getbit命令也知道是如何计算的,过程类似。找到对应的字节数组byte=offset/8;计算对应的bit位bit=(offsetmod8)+1;经过上面的计算,我们可以知道,在执行命令getbittest23时,先计算3/8=0,求出buf[0],然后用(3mod8)+1=4求位。细心的朋友看到??这里会有疑惑,说不对。按照这个计算,返回值应该是0,因为上面setbit命令的结果是00001000。能找到这个问题的朋友解释一下,仔细看了,这里告诉大家,虽然结果是setbit命令是00001000,但是“buf[0]中存储的值确实是倒过来的,是00010000”。相反的顺序用于保存位数组。之所以将位数组倒序保存,是为了减少位数组的移动,提高性能。有兴趣的小伙伴可以自行研究。BITCOUNT命令bitcount命令用于计算位数组中1的个数。说起来比较简单,但是在实现上却很有讲究。我们想象一下,统计位数组中1的个数最简单的方法就是依次遍历累加。但是当我们的位数组很大的时候,整体的效率会变得很慢,因为遍历和长度是正相关的。当存储一个100MB的位数组时,整个遍历需要8亿次。而到了500MB的时候,整个遍历就达到了四十亿次!在Redis中,使用了表查找和可变精度SWAR算法。查表是指当位数组长度小于128时,直接根据预设的映射表查找。对应1的个数,直接返回。变精度SWAR算法比较复杂,阿芬需要进一步研究,今天就不分享了。BITOP命令bitop命令比较简单,因为Redis的底层是基于C语言实现的,而C语言本身就支持相关的逻辑操作。因为是二进制位数组,所以相应的逻辑运算会简单很多,就不详细描述了,相信大家都能看懂。参考资料Redis设计与实现(第二版)
