Redis5设计与源码分析 (第12章 散列表相关命令的实现)

与Redis中的key-value散列做区分,我们称value的散列结构的键值对为field-value(域值对)。

12.1 简介

对于图12-1中的每个key,value指针都是指向redisObject的结构体,该结构体记录了存储结构的类型以及其编码方式等信息,

12.1.1 底层存储

图12-1 key-value底层存储结构图

ziplist和hashtable 存储方式

散列指令对应的底层value的存储并不总是散列表,Redis提供了ziplist以及散列表(hashtable)两种方案进行存储。

当需要存储的key-value结构同时满足下面两个条件时,采用ziplist作为底层存储,否则需要转换为散列表存储。值得注意的是,ziplist的存储顺序与插入顺序一致,而散列表的存储则不一致。

1)key-value结构的所有键值对的字符串长度都小于hash-max-ziplist-value(默认值64),该值可以通过配置文件配置。

2)散列对象保存的键值对的个数(一个键值对记为1个)小于hash-max-ziplist-entries(默认值512),该值也可以通过配置文件配置。

 

当Redis采用ziplist底层存储时,结构图如图12-2所示。图中的f1、v1等字符串只是用于说明ziplist存储的基本结构,具体的存储方式是按照ziplist entry的方式存储,具体详情请见ziplist数据结构。

图12-2 ziplist作为散列命令底层存储结构图

当Redis采用散列表底层存储时,其结构如图12-3所示。此时就是一般的散列表。

图12-3 散列表作为散列命令底层存储

12.1.2 底层存储转换

Redis散列存储有ziplist和散列表2种,有时需要从ziplist编码转换为散列表编码。

但是,后期不会从散列表编码转换为ziplist编码。

具体转换过程如下所示,核心思想是依次迭代原有数据,将其加入到新建的散列表中

void hashTypeConvertZiplist(robj *o, int enc) {

serverAssert(o->encoding == OBJ_ENCODING_ZIPLIST);

if (enc == OBJ_ENCODING_ZIPLIST) {

/* Nothing to do... */

} else if (enc == OBJ_ENCODING_HT) {

hashTypeIterator *hi;

dict *dict;

int ret;

hi = hashTypeInitIterator(o); //初始化迭代器

dict = dictCreate(&hashDictType, NULL); //创建散列表

while (hashTypeNext(hi) != C_ERR) { //遍历所有数据,将其加入到散列表

sds key, value;

key = hashTypeCurrentObjectNewSds(hi,OBJ_HASH_KEY);

value = hashTypeCurrentObjectNewSds(hi,OBJ_HASH_VALUE);

ret = dictAdd(dict, key, value);

}

hashTypeReleaseIterator(hi);

zfree(o->ptr);

o->encoding = OBJ_ENCODING_HT;

o->ptr = dict;

} else {

serverPanic("Unknown hash encoding");

}

}

12.1.3 接口说明

为了使用方便,Redis提供了整合接口以供使用。例如:对于插入指令,Redis提供了hashTypeSet接口,该接口根据存储的编码方式分别调用ziplist的接口或者散列表的接口,本节对这类整合后的接口进行简单整理,具体源码实现此处就不再讲解。

如表12-1所示,Redis提供了大量的整合接口,这类接口会根据散列存储的实际编码(ziplist和散列表)调用相关接口进行处理。后续的命令解析源码中,大量使用该类接口。这部分的源码详见src/t_hash.c文件。

表12-1 散列类型接口说明

需要先找出key对应的value。查找key的接口,如表12-2所示。

表12-2 key查找相关接口

12.2 设置命令

设置散列表中的field-value对,有3个命令:hsethmsethsetnx. 我们在插入新的field-value对时,需要判断是否需要编码转换,如果需要则直接转换编码。之后,我们只需调用ziplist或者散列表的上层接口实现数据插入。

格式:

hset key field value

hmset key field value [field value …]

hsetnx key field value

说明: 将key对应的散列表中的field域设置为value,如果key不存在则新建散列表。field域不存在则新建field域,将其值设置为value,并返回1。

如果field已经存在

hset会覆盖旧值,返回0;hsetnx不进行任何操作,直接返回0;

hmset是hset的批量设置形式,成功返回ok。

 

设置类命令的源码实现:

hset、hsetnx对应的源码实现的入口函数分别是hsetCommandhsetnxCommand。hmset的源码实现的入口也是hsetCommand。

三者实现的基本流程大致相同,首先判断key是否存在,不存在则直接创建;之后,根据field域是否存在,选择覆盖旧的数据或者直接返回。

hsetCommand的源码如下所示:

①查找key是否存在,不存在则直接新建;

②尝试是否需要将ziplist编码改为散列表编码;

③将需要写入的field、value依次写入;

④向客户端发送处理结果。

//查找key,不存在则新建

if ((o = hashTypeLookupWriteOrCreate(c,c->argv[1])) == NULL) return;

// 尝试转换底层编码

hashTypeTryConversion(o,c->argv,2,c->argc-1);

//依次写入field-value对

for (i = 2; i < c->argc; i += 2)

created += !hashTypeSet(o,c->argv[i]->ptr,c->argv[i+1]->ptr,HASH_SET_COPY);

 

hsetnxCommand的实现不同点在于hsetnxCommand会判断field是否已经存在,如果不存在则插入,存在则不进行任何操作,直接返回。

12.3 读取命令

获取field-value的信息。具体而言,命令有如下5类。

① hexists:判断field域是否存在。

② hget/hmget:获取单个field对应的value以及获取批量field对应的value。

③ hkeys/hvals/hgetall:获取全部的field、value或者field-value对。

④hlen:获取field的总个数。

⑤hscan:遍历hscan。

12.3.1 hexists命令

查看某个field是否存在,可以用于标识某个操作之前是否已经执行过。

格式:hexists key field

说明: 查看field是否存在,存在返回1,key不存在或者field不存在返回0。

示例:

> hset tom name tom1

1

> hexists tom name

1

源码解析:

hexists的源码处理函数为hexitsCommand,基本流程就是先获取key对应的散列表,之后判断field是否存在:

void hexistsCommand(client *c) {

robj *o;

if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.czero)) == NULL ||

checkType(c,o,OBJ_HASH)) return;

addReply(c, hashTypeExists(o,c->argv[2]->ptr) ? shared.cone : shared.czero);

}

12.3.2 hget/hmget命令

获取单个field或者多个field对应的value值。

格式:

hget key field

hmget key field [field …]

说明: 获取单个或者多个field域对应的value值,如果key不存在则返回nil,如果某个field不存在,则该field对应的位置返回nil。示例:

> hget tom name

tom1

> hmget tom name id

tom1

null

源码解析:

hget、hmget对应的处理函数分别为hgetCommand、hmgetCommand。二者相似,此处仅给出hmgetCommand的源码分析。

首先读取key对应的散列表,之后遍历所有field,返回对应的value或者nil。

void hmgetCommand(client *c) {

robj *o;

int i;

/* key不存在不要中断,返回nil. */

o = lookupKeyRead(c->db, c->argv[1]); //读取key对应的散列表

if (o != NULL && o->type != OBJ_HASH) {

addReply(c, shared.wrongtypeerr);

return;

}

addReplyArrayLen(c, c->argc-2);

//遍历所有field, 返回对应的value或者nil

for (i = 2; i < c->argc; i++) {

addHashFieldToReply(c, o, c->argv[i]->ptr);

}

}

12.3.3 hkeys/hvals/hgetall命

获取某个key下的所有field-value对的信息;

hkeys可以获取某个key下的所有field信息;

hvals可以获取每个field对应的value信息;

hgetall则可以获取所有的field-value对。

格式:

hkeys key

hvals key

hgetall key

提示  返回顺序没有固定顺序,取决于具体的底层编码

源码分析:

底层实现都是genericHgetallCommand函数,通过flags决定输出key或value还是二者都输出,具体源码如下,通过迭代器遍历散列表将所有field-value输出:

void genericHgetallCommand(client *c, int flags) {

robj *o;

hashTypeIterator *hi;

int length, count = 0;

 

//获取初始化迭代器

hi = hashTypeInitIterator(o);

//遍历散列表

while (hashTypeNext(hi) != C_ERR) {

if (flags & OBJ_HASH_KEY) {

addHashIteratorCursorToReply(c, hi, OBJ_HASH_KEY);

count++;

}

if (flags & OBJ_HASH_VALUE) {

addHashIteratorCursorToReply(c, hi, OBJ_HASH_VALUE);

count++;

}

}

}

12.3.4 hlen命令

hlen命令可以用于获取散列表中field的个数,主要用于数据统计。

格式:

hlen key

说明: 查key对应的散列表中field的个数,key不存在则返回0。

源码分析:

函数为hlenCommand,基本流程就是先获取key对应的散列表,之后通过hashTypeLength接口获取field个数,此处源码就不再展示。

/* Return the number of elements in a hash. */

unsigned long hashTypeLength(const robj *o) {

unsigned long length = ULONG_MAX;

if (o->encoding == OBJ_ENCODING_ZIPLIST) {

length = ziplistLen(o->ptr) / 2;

} else if (o->encoding == OBJ_ENCODING_HT) {

length = dictSize((const dict*)o->ptr);

} else {

serverPanic("Unknown hash encoding");

}

return length;

}

12.3.5 hscan命令

遍历散列表中所有的field-value对,hscan命令是渐进式遍历(当底层存储为散列表时)。

格式:

hscan key cursor [MATCH pattern] [COUNT count]

说明:

用于遍历key指向的散列表。

cursor指向当前的位置,0代表新一轮的迭代,返回0代表本轮迭代结束;count是需要返回的field个数,默认值是10,当底层编码为ziplist时,该值无效,Redis会将

ziplist中所有field-value返回,当编码为散列表时,返回的元素个数不一定,可能大于,也可能小于或等于此值;

pattern是需要匹配的模式,这一步是读取完数据之后,发送数据之前执行的。

示例:

源码分析:

处理函数为hscanCommand,其功能是通过调用scanGeneric-Command实现的。函数的处理流程为:

1)当编码为散列表时,逐个遍历散列表的桶,得到其中的元素,当遍历到散列表末尾、达到count个元素或者达到最大迭代次数后终止;

2)当编码为ziplist时,输出所有元素;

3)对上一步的元素进行过滤,将结果发送给客户端。

void scanGenericCommand(client *c, robj *o, unsigned long cursor) {

/* Step 1: Parse options. */

....

/* Step 2: Iterate the collection.

/* Handle the case of a hash table. */

if (ht) { //编码为散列表的情况

void *privdata[2];

/* 如果对象是用ziplist,intset或不是哈希表的任何其他表示形式编码的,可以确定它也是由少量元素组成的。因此,为了避免进入状态,我们只需在一次调用中返回对象内部的所有内容,并将光标设置为零以表示迭代结束 */

long maxiterations = count*10;

/*将两个指针传递给回调:将向其添加新元素的列表,以及包含字典的对象,以与类型有关的方式获取更多数据。 */

privdata[0] = keys;

privdata[1] = o;

do {

cursor = dictScan(ht, cursor, scanCallback, NULL, privdata);

} while (cursor &&

maxiterations-- &&

listLength(keys) < (unsigned long)count);

} else if (o->type == OBJ_SET) {

int pos = 0;

int64_t ll;

while(intsetGet(o->ptr,pos++,&ll))

listAddNodeTail(keys,createStringObjectFromLongLong(ll));

cursor = 0;

} else if (o->type == OBJ_HASH || o->type == OBJ_ZSET) {

//编码为ziplist的情况

unsigned char *p = ziplistIndex(o->ptr,0);

unsigned char *vstr;

unsigned int vlen;

long long vll;

//遍历ziplist所有元素

while(p) {

ziplistGet(p,&vstr,&vlen,&vll);

listAddNodeTail(keys,

(vstr != NULL) ? createStringObject((char*)vstr,vlen) :

createStringObjectFromLongLong(vll));

p = ziplistNext(o->ptr,p);

}

cursor = 0;

}

/* Step 3: Filter elements. */

...

/* Step 4: Reply to the client. */

     ...

cleanup:

listSetFreeMethod(keys,decrRefCountVoid);

listRelease(keys);

}

12.4 删除命令

hdel直接调用ziplist或者散列表的接口将数据删除。

格式:

hdel key field [field …]

说明: 将key对应的散列表中的field删除,key为空时返回0,key不为空时返回成功删除的field个数。

提示  当散列表中field全部被删除时,key也会被删除。

源码分析:

hdel命令对应的处理函数为hdelCommand,基本的处理逻辑直接调用底层提供的接口进行数据删除,源码实现如下:

void hdelCommand(client *c) {

robj *o;

int j, deleted = 0, keyremoved = 0;

//查找key并检查其类型

if ((o = lookupKeyWriteOrReply(c,c->argv[1],shared.czero)) == NULL ||

checkType(c,o,OBJ_HASH)) return;

for (j = 2; j < c->argc; j++) { //依次删除field-value对

if (hashTypeDelete(o,c->argv[j]->ptr)) {

deleted++;

if (hashTypeLength(o) == 0) { //散列表已经为空,删除key

dbDelete(c->db,c->argv[1]);

keyremoved = 1;

break;

}

}

}

}

12.5 自增命令

当散列表field-value中的field为整数或者浮点型的数据时,对其中的value进行加法操作(减法可以视为加上一个负数),Redis提供了hincrby、hincrbyfloat两个命令。

格式:

hincrby key field increment

hincrbyfloat key field increment

说明:

将field对应的value增加increment,当为浮点型数据时使用hincrbyfloat。如果key不存在则直接新建key,field不存在则直接新建field,设置其值为0。如果hincrby增加后数值越界,则返回错误,不进行操作。hincrbyfloat不进行越界判断。如果field对应的value类型不是数值,则直接返回错误。命令返回增加后的新值

提示  整数使用long long,浮点型使用long double。原来的值和增加的值都是整型时,才可以使用hincrby。

源码分析:

hincybyCommand源码如下:首先获取key对应的散列表,之后从散列表中读取原有的field对应的值,然后判断增加之后是否越界,最后将增加后的值写入field域。

void hincrbyCommand(client *c) {

long long value, incr, oldvalue; robj *o; sds new;

unsigned char *vstr;     unsigned int vlen;

if (getLongLongFromObjectOrReply(c,c->argv[3],&incr,NULL) != C_OK) return;

     //读取key对应的value

if ((o = hashTypeLookupWriteOrCreate(c,c->argv[1])) == NULL) return;

//获取field域中原来的值

if (hashTypeGetValue(o,c->argv[2]->ptr,&vstr,&vlen,&value) == C_OK) {

//判断value类型

} /* Else hashTypeGetValue() already stored it into &value */

} else {

value = 0;

}

oldvalue = value;

//判断是否越界

if ((incr < 0 && oldvalue < 0 && incr < (LLONG_MIN-oldvalue)) ||

(incr > 0 && oldvalue > 0 && incr > (LLONG_MAX-oldvalue))) {

addReplyError(c,"increment or decrement would overflow");

return;

}

value += incr;

new = sdsfromlonglong(value);

//设置新值

hashTypeSet(o,c->argv[2]->ptr,new,HASH_SET_TAKE_VALUE);

}

hincrbyfloat对应的源码基本与hincrby相似,不同点在于,hincryfloat是对浮点型数据进行操作,也没有对数值越界进行处理

12.6 本章小结

Redis对外提供的散列相关命令的底层实现。

Redis对散列结构的存储方式,即ziplist或者散列表,当field-value长度较短并且field-value的个数较少时,Redis采用ziplist用于存储,否则使用散列表。

Redis为使用方便,整合这两种结构后对外提供的统一接口。

Redis是如何利用上述整合接口实现散列相关命令的。

  

原文地址:https://www.cnblogs.com/coloz/p/13812850.html