环境说明:redis源码版本 5.0.3;我在阅读源码过程做了注释,git地址:https://gitee.com/xiaoangg/redis_annotation
如有错误欢迎指正
参考书籍:《redis的设计与实现》推荐阅读:
redis-Bitmaps 基础概念汉明重量
文章推荐:
redis源码阅读-一--sds简单动态字符串
redis源码阅读--二-链表
redis源码阅读--三-redis散列表的实现
redis源码浅析--四-redis跳跃表的实现
redis源码浅析--五-整数集合的实现
redis源码浅析--六-压缩列表
redis源码浅析--七-redisObject对象(下)(内存回收、共享)
redis源码浅析--八-数据库的实现
redis源码浅析--九-RDB持久化
redis源码浅析--十-AOF(append only file)持久化
redis源码浅析--十一.事件(上)文件事件
redis源码浅析--十一.事件(下)时间事件
redis源码浅析--十二.单机数据库的实现-客户端
redis源码浅析--十三.单机数据库的实现-服务端 - 时间事件
redis源码浅析--十三.单机数据库的实现-服务端 - redis服务器的初始化
redis源码浅析--十四.多机数据库的实现(一)--新老版本复制功能的区别与实现原理
redis源码浅析--十四.多机数据库的实现(二)--复制的实现SLAVEOF、PSYNY
redis源码浅析--十五.哨兵sentinel的设计与实现
redis源码浅析--十六.cluster集群的设计与实现
redis源码浅析--十七.发布与订阅的实现
redis源码浅析--十八.事务的实现
redis源码浅析--十九.排序的实现
redis源码浅析--二十.BIT MAP的实现
redis源码浅析--二十一.慢查询日志的实现
redis源码浅析--二十二.监视器的实现
目录
redis提供了SETBIT、GETBIT、BITCOUNT、BITOP 、BITFIELD(起始版本3.2.0)、BITPOS 等命令处理二进制位数组;
一 位数组的表示
redis使用字符串对象来表示位数组;因为SDS是二进制安全的,所以程序直接使用SDS结构来保存位数组,并使用SDS的操作函数 实现位操作;
Bitmaps本身不是一种数据结构, 实际上它就是字符串;
二 GETBIT命令的实现
getbit命令返回位数组,在offset偏移量上的值,注意offset是从0开始
getbit命令语法;
GETBIT <KEY> <OFFSET>
因为bitmap的本质其实字符串, getbit命令的核心是将offset转换成对应的byte位置和bit位置;
gitbit命令的实现入口位于bitop.c/getbitCommand
/**
* GETBIT 命令的实现
*/
/* GETBIT key offset */
void getbitCommand(client *c) {
robj *o;
char llbuf[32];
size_t bitoffset; //
size_t byte, bit;
size_t bitval = 0;
//解析命令中的offset
if (getBitOffsetFromArgument(c,c->argv[2],&bitoffset,0,0) != C_OK)
return;
//查找命令中的key ,并检查编码方式是否是 OBJ_STRING
if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.czero)) == NULL ||
checkType(c,o,OBJ_STRING)) return;
//byte = bitoffset/8; 记录偏移量位于哪个字节
byte = bitoffset >> 3;
//bit对应的位, 举个列子 如 bitoffset是5,则取的值从右数第第5位,左数第2位
bit = 7 - (bitoffset & 0x7);
if (sdsEncodedObject(o)) {
if (byte < sdslen(o->ptr))
bitval = ((uint8_t*)o->ptr)[byte] & (1 << bit);
} else {
if (byte < (size_t)ll2string(llbuf,sizeof(llbuf),(long)o->ptr))
bitval = llbuf[byte] & (1 << bit);
}
addReply(c, bitval ? shared.cone : shared.czero);
}
三 SETBIT命令的实现
setbit命令用于将位数组上,偏移量设置成指定的值value;
语法:
SETBIT <key> <offset> <value>
setbit命令执行过程如下:
- 计算offset对应的byte位和bit位;
- 检查offset是否超过了当前SDS的长度,如果超出了,将扩容SDS,并初始化扩容部分为0;
- 获取当前offset位的值;
- 设置offset位 的新的值,并返回老的值;
setbit命令的实现 bitop.c/setbitCommand
/**
* SETBIT命令的实现
*/
/* 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;
//获取 offset 参数
if (getBitOffsetFromArgument(c,c->argv[2],&bitoffset,0,0) != C_OK)
return;
//获取 key参数 对象
if (getLongFromObjectOrReply(c,c->argv[3],&on,err) != C_OK)
return;
//on & ~1 为真时候,说明 on 不是 0 或者 1; 说明参数错误
/* Bits can only be set or cleared... */
if (on & ~1) {
addReplyError(c,err);
return;
}
//查找key 并判断offset是否在超过了key sds的长度,
if ((o = lookupStringForBitCommand(c,bitoffset)) == NULL) return;
//获取 offset为当前的值
/* Get current values */
byte = bitoffset >> 3;
byteval = ((uint8_t*)o->ptr)[byte];
bit = 7 - (bitoffset & 0x7);
bitval = byteval & (1 << bit);
/**
* 设置offset位为新值
*/
/* Update byte with new bit value and return original value */
byteval &= ~(1 << bit);
byteval |= ((on & 0x1) << bit);
((uint8_t*)o->ptr)[byte] = byteval;
//通知key发生变更
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);
}
四 BITCOUNT 命令的实现
bitcount用于统计给定位数组中 ,值位1的二进制位数量;
语法:
BITCOUNT key [start] [end]
bitcount乍看上去不复杂,但是要高效的的实现这个命令并不容易;
1.遍历算法
实现bitcount的最简单的方法就是 遍历数组中的每个二进制位,遇到值为1的,将计数器+1;
遍历算法实现简单,但效率极低;1MB = 1024^2 Byte; 统计1MB的字符,就要8* 1024^2 bit操作;
2.查表法
查表法原理
- 对于个有限集合,集合的排列方式是有限的;
- 对于一个有限长度的位数组来说,他能表示的二进制位排列也是有限;
根据这个原理,我们可以创建一个表,表的键是某种排列的位数组,值是对应位值为1的数量;
举个例子,我们创建下面一个8位长的位数组表
通过这个表我们可以一次性读入8位 进行查表,就能直接得出这个值包含1的数量;
优点:
对比遍历算法,我们一次性就能统计8位二进制位,性能提升了8倍;
如果我们建个更大的表,一次性检查的位就更多了;
缺点:
乍看 只要建立一个更大表,但是查表发会受到内存和缓存两方面的限制:
- 查表法是典型的空间换时间策略,节约时间越多,花费的额外内存就越大;
创建8位的表需要几百个字节,创建16位的需要几百KB,创建32位的表,则需要十多个G。
服务器能接受的内存消耗是有限的; - cpu缓存的限制;
对于固定大小的cpu缓存来说,创建表越大,cpu缓存能保存的内容 相比整个表比例就越少;
查表出现缓存不命中(cache miss)就越高,缓存的换入和换出操作就越频繁,影响查表法实际效率;
结论:
查表法相比遍历法效率更高,但受到内存大小和cpu缓存的限制,对于处理非常长的位数组,效率远远不够;
3 variable-precision SWAR 算法
汉明重量
汉明重量是一串符号中非零符号的个数。因此它等同于同样长度的全零符号串的汉明距离。在最为常见的数据位符号串中,它是1的个数。
可以看到 bitcount要解决的问题 在数学上被称为汉明重量。 目前已知效率最好的解法是是variable-precision SWAR 算法;
/**
*
* 统计二进制数组中由“s”和长“Count”字节指向的位数。
* 该函数的实现需要使用输入字符串长度达到512 MB。
*
* s:统计字符起始位置
* count:总的byte数量,例如 count=8,从 s开始,共统计8 byte
*
* //TODO “汉明重量” sware算法
* https://segmentfault.com/a/1190000015481454
*/
/* 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; //计数器 统计1的数量
unsigned char *p = s;
uint32_t *p4;
//键长8位的hash表,记录所有 0000 0000 到1111 1111 的汉明重量
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};
/**
* //TODO p & 3 啥意思 四字节对齐?
*/
/* Count initial bytes not aligned to 32 bit. */
while((unsigned long)p & 3 && count) {
bits += bitsinbyte[*p++];
count--;
}
/**
* 一次性统计 28byte的
*/
/* 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;
}
//统计剩余的byte
/* Count the remaining bytes. */
p = (unsigned char*)p4;
while(count--) bits += bitsinbyte[*p++];
return bits;
}
五 BITOP命令的实现
BITOP对一个或多个保存二进制位的字符串 key
进行位元操作,并将结果保存到指定key上;
语法:
BITOP <op_name> <target_key> <src_key1> [src_key2 src_key3 ... src_keyN]
op_name可以是 AND 、 OR 、 NOT 、 XOR 这四种操作中的任意一种:
C语言支持直接对字节执行 &(逻辑与)、|(逻辑或)、^(异或)、~(逻辑非)
redis bitop全都是基于这些操作实现的;
因为 AND 、OR、XOR 支持多个key操作,并且需要遍历每个key的每个字节进行操作,所以时间复杂度是O(n*n);
NOT操作支持一个key,所以时间复杂度是O(n);