Redis(五):hash/hset/hget 命令源码解析
Redis作为nosql数据库,kv string型数据的支持是最基础的,但是如果仅有kv的操作,也不至于有redis的成功。(memcache就是个例子)
Redis除了string, 还有hash,list,set,zset。
所以,我们就来看看hash的相关操作实现吧。
首先,我们从作用上理解hash存在的意义:Redis hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象。从另一个方面来说是,hash可以聚合很多类似的属性,这是string中难以实现的。
所以,总体来说,hash的命令与string的命令差不太多。其操作手册如下:
1> hdel 命令:删除一个或多个哈希表字段
格式:HDEL key field2 [field2]
返回值:被成功删除字段的数量,不包括被忽略的字段。2> hexists 命令:查看哈希表 key 中,指定的字段是否存在
格式:HEXISTS key field
返回值:如果哈希表含有给定字段,返回 1 。如果哈希表不含有给定字段,或 key 不存在,返回 0 。3> hget 命令:获取存储在哈希表中指定字段的值
格式:HGET key field
返回值:返回给定字段的值。如果给定的字段或 key 不存在时,返回 nil 。4> hgetall 命令:获取在哈希表中指定 key 的所有字段和值
格式:HGETALL key
返回值:以列表形式返回哈希表的字段及字段值。若 key 不存在,返回空列表。5> hincrby 命令:为哈希表 key 中的指定字段的整数值加上增量 increment
格式:HINCRBY key field increment
返回值:执行 HINCRBY 命令之后,哈希表中字段的值。6> hincrbyfloat 命令:为哈希表 key 中的指定字段的浮点数值加上增量 increment
格式:HINCRBYFLOAT key field increment
返回值:执行 Hincrbyfloat 命令之后,哈希表中字段的值。7> hkeys 命令:获取所有哈希表中的字段
格式:HKEYS key
返回值:包含哈希表中所有字段的列表。当 key 不存在时,返回一个空列表。8> hlen 命令:获取哈希表中字段的数量
格式:HLEN key
返回值:哈希表中字段的数量。当 key 不存在时,返回 0 。9> hmget 命令:获取所有给定字段的值
格式:HMGET key field1 [field2]
返回值:一个包含多个给定字段关联值的表,表值的排列顺序和指定字段的请求顺序一样。10> hmset 命令:同时将多个 field-value (域-值)对设置到哈希表 key 中
格式:HMSET key field1 value1 [field2 value2 ]
返回值:如果命令执行成功,返回 OK 。11> hset 命令:将哈希表 key 中的字段 field 的值设为 value
格式:HSET key field value
返回值:如果字段是哈希表中的一个新建字段,并且值设置成功,返回 1 。如果哈希表中域字段已经存在且旧值已被新值覆盖,返回 0 。12> hsetnx 命令:只有在字段 field 不存在时,设置哈希表字段的值
格式:HSETNX key field value
返回值:设置成功,返回 1 。如果给定字段已经存在且没有操作被执行,返回 0 。13> hvals 命令:获取哈希表中所有值
格式:HVALS key
返回值:一个包含哈希表中所有值的表。当 key 不存在时,返回一个空表。14> hscan 命令:迭代哈希表中的键值对
格式:HSCAN key cursor [MATCH pattern] [COUNT count]
其中,有的是单kv操作有的是指量操作,有的是写操作有的是读操作。从实现上看,大体上很多命令是类似的:
比如:hset/hmset/hincrbyXXX 可以是一类的
比如:hget/hgetall/hexists/hkeys/hmget 可以是一类
注意:以上分法仅是为了让我们看清本质,对实际使用并无实际参考意义。
所以,我们就挑几个方法来解析下 hash 的操作实现吧。
零、hash数据结构
hash相关的命令定义如下:
{"hset",hsetCommand,4,"wmF",0,NULL,1,1,1,0,0},{"hsetnx",hsetnxCommand,4,"wmF",0,NULL,1,1,1,0,0},{"hget",hgetCommand,3,"rF",0,NULL,1,1,1,0,0},{"hmset",hmsetCommand,-4,"wm",0,NULL,1,1,1,0,0},{"hmget",hmgetCommand,-3,"r",0,NULL,1,1,1,0,0},{"hincrby",hincrbyCommand,4,"wmF",0,NULL,1,1,1,0,0},{"hincrbyfloat",hincrbyfloatCommand,4,"wmF",0,NULL,1,1,1,0,0},{"hdel",hdelCommand,-3,"wF",0,NULL,1,1,1,0,0},{"hlen",hlenCommand,2,"rF",0,NULL,1,1,1,0,0},{"hstrlen",hstrlenCommand,3,"rF",0,NULL,1,1,1,0,0},{"hkeys",hkeysCommand,2,"rS",0,NULL,1,1,1,0,0},{"hvals",hvalsCommand,2,"rS",0,NULL,1,1,1,0,0},{"hgetall",hgetallCommand,2,"r",0,NULL,1,1,1,0,0},{"hexists",hexistsCommand,3,"rF",0,NULL,1,1,1,0,0},{"hscan",hscanCommand,-3,"rR",0,NULL,1,1,1,0,0},
ziplist 数据结构
typedef struct zlentry {unsigned int prevrawlensize, prevrawlen;unsigned int lensize, len;unsigned int headersize;unsigned char encoding;unsigned char *p;} zlentry;#define ZIPLIST_BYTES(zl) (*((uint32_t*)(zl)))#define ZIPLIST_TAIL_OFFSET(zl) (*((uint32_t*)((zl)+sizeof(uint32_t))))#define ZIPLIST_LENGTH(zl) (*((uint16_t*)((zl)+sizeof(uint32_t)*2)))#define ZIPLIST_HEADER_SIZE (sizeof(uint32_t)*2+sizeof(uint16_t))#define ZIPLIST_END_SIZE (sizeof(uint8_t))#define ZIPLIST_ENTRY_HEAD(zl) ((zl)+ZIPLIST_HEADER_SIZE)#define ZIPLIST_ENTRY_TAIL(zl) ((zl)+intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl)))#define ZIPLIST_ENTRY_END(zl) ((zl)+intrev32ifbe(ZIPLIST_BYTES(zl))-1)
hashtable 数据结构:
typedef struct dict {dictType *type;void *privdata;dictht ht[2];long rehashidx; /* rehashing not in progress if rehashidx == -1 */unsigned long iterators; /* number of iterators currently running */} dict;typedef struct dictht {dictEntry **table;unsigned long size;unsigned long sizemask;unsigned long used;} dictht;typedef struct dictEntry {void *key;void *val;struct dictEntry *next;} dictEntry;
一、hset 设置单个 field -> value
“增删改查”中的“增改” 就是它了。
// t_hash.c, set key field valuevoid hsetCommand(client *c) {int update;robj *o;// 1. 查找hash的key是否存在,不存在则新建一个,然后在其上进行数据操作if ((o = hashTypeLookupWriteOrCreate(c,c->argv[1])) == NULL) return;// 2. 检查2-3个参数是否需要将简单版(ziplist)hash表转换为复杂的hash表,转换后的表通过 o->ptr 体现hashTypeTryConversion(o,c->argv,2,3);// 3. 添加kv到 o 的hash表中update = hashTypeSet(o,c->argv[2]->ptr,c->argv[3]->ptr,HASH_SET_COPY);addReply(c, update ? shared.czero : shared.cone);// 变更命令传播signalModifiedKey(c->db,c->argv[1]);notifyKeyspaceEvent(NOTIFY_HASH,"hset",c->argv[1],c->db->id);server.dirty++;}// 1. 获取db外部的key, 即整体hash数据实例// t_hash.crobj *hashTypeLookupWriteOrCreate(client *c, robj *key) {robj *o = lookupKeyWrite(c->db,key);if (o == NULL) {// 此处创建的hashObject是以 ziplist 形式的o = createHashObject();dbAdd(c->db,key,o);} else {// 不是hash类型的键已存在,不可覆盖,返回错误if (o->type != OBJ_HASH) {addReply(c,shared.wrongtypeerr);return NULL;}}return o;}// object.c, 创建hashObject, 以 ziplist 形式创建robj *createHashObject(void) {unsigned char *zl = ziplistNew();robj *o = createObject(OBJ_HASH, zl);o->encoding = OBJ_ENCODING_ZIPLIST;return o;}// ziplist.cstatic unsigned char *createList() {unsigned char *zl = ziplistNew();zl = ziplistPush(zl, (unsigned char*)"foo", 3, ZIPLIST_TAIL);zl = ziplistPush(zl, (unsigned char*)"quux", 4, ZIPLIST_TAIL);zl = ziplistPush(zl, (unsigned char*)"hello", 5, ZIPLIST_HEAD);zl = ziplistPush(zl, (unsigned char*)"1024", 4, ZIPLIST_TAIL);return zl;}// 2. 检查参数,是否需要将 ziplist 形式的hash表转换为真正的hash表/* Check the length of a number of objects to see if we need to convert a* ziplist to a real hash. Note that we only check string encoded objects* as their string length can be queried in constant time. */void hashTypeTryConversion(robj *o, robj **argv, int start, int end) {int i;if (o->encoding != OBJ_ENCODING_ZIPLIST) return;for (i = start; i <= end; i++) {// 参数大于设置的 hash_max_ziplist_value (默认: 64)时,会直接将 ziplist 转换为 ht// OBJ_ENCODING_RAW, OBJ_ENCODING_EMBSTR// 循环检查参数,只要发生了一次转换就结束检查(没必要继续了)if (sdsEncodedObject(argv[i]) &&sdslen(argv[i]->ptr) > server.hash_max_ziplist_value){// 这个转换过程很有意思,我们深入看看hashTypeConvert(o, OBJ_ENCODING_HT);break;}}}// t_hash.c, 转换编码方式 (如上, ziplist -> ht)void hashTypeConvert(robj *o, int enc) {if (o->encoding == OBJ_ENCODING_ZIPLIST) {// 此处我们只处理这种情况hashTypeConvertZiplist(o, enc);} else if (o->encoding == OBJ_ENCODING_HT) {serverPanic("Not implemented");} else {serverPanic("Unknown hash encoding");}}// t_hash.c, 转换编码 ziplist 为目标 enc (实际只能是 OBJ_ENCODING_HT)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);// 一个hash的数据结构就是一个 dict, 从这个级别来说, hash 与 db 是一个级别的dict = dictCreate(&hashDictType, NULL);// 依次迭代 o, 赋值到 hi->fptr, hi->vptr// 依次添加到 dict 中while (hashTypeNext(hi) != C_ERR) {sds key, value;// 从 hi->fptr 中获取key// 从 hi->vptr 中获取valuekey = hashTypeCurrentObjectNewSds(hi,OBJ_HASH_KEY);value = hashTypeCurrentObjectNewSds(hi,OBJ_HASH_VALUE);// 添加到 dict 中ret = dictAdd(dict, key, value);if (ret != DICT_OK) {serverLogHexDump(LL_WARNING,"ziplist with dup elements dump",o->ptr,ziplistBlobLen(o->ptr));serverPanic("Ziplist corruption detected");}}// 释放迭代器hashTypeReleaseIterator(hi);zfree(o->ptr);// 将变更反映到o对象上返回o->encoding = OBJ_ENCODING_HT;o->ptr = dict;} else {serverPanic("Unknown hash encoding");}}// 2.1. 迭代ziplist元素// t_hash.c, 迭代器/* Move to the next entry in the hash. Return C_OK when the next entry* could be found and C_ERR when the iterator reaches the end. */int hashTypeNext(hashTypeIterator *hi) {if (hi->encoding == OBJ_ENCODING_ZIPLIST) {unsigned char *zl;unsigned char *fptr, *vptr;// 每次都是基于原始字符器进行计算偏移// 迭代的是 fptr,vptrzl = hi->subject->ptr;fptr = hi->fptr;vptr = hi->vptr;// 第一次查找时使用index查找,后续则使用 fptr,vptr 进行迭代if (fptr == NULL) {/* Initialize cursor */serverAssert(vptr == NULL);fptr = ziplistIndex(zl, 0);} else {/* Advance cursor */serverAssert(vptr != NULL);fptr = ziplistNext(zl, vptr);}if (fptr == NULL) return C_ERR;/* Grab pointer to the value (fptr points to the field) */vptr = ziplistNext(zl, fptr);serverAssert(vptr != NULL);/* fptr, vptr now point to the first or next pair */hi->fptr = fptr;hi->vptr = vptr;} else if (hi->encoding == OBJ_ENCODING_HT) {if ((hi->de = dictNext(hi->di)) == NULL) return C_ERR;} else {serverPanic("Unknown hash encoding");}return C_OK;}// ziplist.c, 查找 index 的元素/* Returns an offset to use for iterating with ziplistNext. When the given* index is negative, the list is traversed back to front. When the list* doesn't contain an element at the provided index, NULL is returned. */unsigned char *ziplistIndex(unsigned char *zl, int index) {unsigned char *p;unsigned int prevlensize, prevlen = 0;if (index < 0) {// 小于0时,反向查找index = (-index)-1;p = ZIPLIST_ENTRY_TAIL(zl);if (p[0] != ZIP_END) {ZIP_DECODE_PREVLEN(p, prevlensize, prevlen);while (prevlen > 0 && index--) {p -= prevlen;ZIP_DECODE_PREVLEN(p, prevlensize, prevlen);}}} else {p = ZIPLIST_ENTRY_HEAD(zl);while (p[0] != ZIP_END && index--) {p += zipRawEntryLength(p);}}// 迭代完成还没找到元素 p[0]=ZIP_END// index 超出整体ziplist大小则遍历完成后 index>0return (p[0] == ZIP_END || index > 0) ? NULL : p;}// ziplist.c, 由 fptr,vptr 进行迭代元素/* Return pointer to next entry in ziplist.** zl is the pointer to the ziplist* p is the pointer to the current element** The element after 'p' is returned, otherwise NULL if we are at the end. */unsigned char *ziplistNext(unsigned char *zl, unsigned char *p) {((void) zl);/* "p" could be equal to ZIP_END, caused by ziplistDelete,* and we should return NULL. Otherwise, we should return NULL* when the *next* element is ZIP_END (there is no next entry). */if (p[0] == ZIP_END) {return NULL;}// 当前指针偏移当前元素长度(根据ziplist协议),即到下一元素指针位置p += zipRawEntryLength(p);if (p[0] == ZIP_END) {return NULL;}return p;}/* Return the total number of bytes used by the entry pointed to by 'p'. */static unsigned int zipRawEntryLength(unsigned char *p) {unsigned int prevlensize, encoding, lensize, len;ZIP_DECODE_PREVLENSIZE(p, prevlensize);ZIP_DECODE_LENGTH(p + prevlensize, encoding, lensize, len);return prevlensize + lensize + len;}// 2.2. t_hash.c, 获取 hashTypeIterator 的具体值,写入 vstr, vlen 中/* Return the key or value at the current iterator position as a new* SDS string. */sds hashTypeCurrentObjectNewSds(hashTypeIterator *hi, int what) {unsigned char *vstr;unsigned int vlen;long long vll;hashTypeCurrentObject(hi,what,&vstr,&vlen,&vll);if (vstr) return sdsnewlen(vstr,vlen);return sdsfromlonglong(vll);}/* Higher level function of hashTypeCurrent*() that returns the hash value* at current iterator position.** The returned element is returned by reference in either *vstr and *vlen if* it's returned in string form, or stored in *vll if it's returned as* a number.** If *vll is populated *vstr is set to NULL, so the caller* can always check the function return by checking the return value* type checking if vstr == NULL. */void hashTypeCurrentObject(hashTypeIterator *hi, int what, unsigned char **vstr, unsigned int *vlen, long long *vll) {if (hi->encoding == OBJ_ENCODING_ZIPLIST) {*vstr = NULL;hashTypeCurrentFromZiplist(hi, what, vstr, vlen, vll);} else if (hi->encoding == OBJ_ENCODING_HT) {sds ele = hashTypeCurrentFromHashTable(hi, what);*vstr = (unsigned char*) ele;*vlen = sdslen(ele);} else {serverPanic("Unknown hash encoding");}}// t_hash.c, 从ziplist中获取某个 hashTypeIterator 的具体值,结果定稿 vstr, vlen/* Get the field or value at iterator cursor, for an iterator on a hash value* encoded as a ziplist. Prototype is similar to `hashTypeGetFromZiplist`. */void hashTypeCurrentFromZiplist(hashTypeIterator *hi, int what,unsigned char **vstr,unsigned int *vlen,long long *vll){int ret;serverAssert(hi->encoding == OBJ_ENCODING_ZIPLIST);// OBJ_HASH_KEY 从 fptr 中获取, 否则从 vptr 中获取if (what & OBJ_HASH_KEY) {ret = ziplistGet(hi->fptr, vstr, vlen, vll);serverAssert(ret);} else {ret = ziplistGet(hi->vptr, vstr, vlen, vll);serverAssert(ret);}}// ziplist.c,/* Get entry pointed to by 'p' and store in either '*sstr' or 'sval' depending* on the encoding of the entry. '*sstr' is always set to NULL to be able* to find out whether the string pointer or the integer value was set.* Return 0 if 'p' points to the end of the ziplist, 1 otherwise. */unsigned int ziplistGet(unsigned char *p, unsigned char **sstr, unsigned int *slen, long long *sval) {zlentry entry;if (p == NULL || p[0] == ZIP_END) return 0;if (sstr) *sstr = NULL;// 按照ziplist的编码协议, 获取头部信息zipEntry(p, &entry);if (ZIP_IS_STR(entry.encoding)) {if (sstr) {*slen = entry.len;*sstr = p+entry.headersize;}} else {if (sval) {*sval = zipLoadInteger(p+entry.headersize,entry.encoding);}}return 1;}// ziplist.c, 解析原始字符串为 zlentry/* Return a struct with all information about an entry. */static void zipEntry(unsigned char *p, zlentry *e) {// 按照ziplist的编码协议,依次读取 prevrawlensize, prevrawlenZIP_DECODE_PREVLEN(p, e->prevrawlensize, e->prevrawlen);// 指向下一位置偏移,按照ziplist的编码协议,依次读取 encoding, lensize, lenZIP_DECODE_LENGTH(p + e->prevrawlensize, e->encoding, e->lensize, e->len);// 除去header得到 body偏移e->headersize = e->prevrawlensize + e->lensize;e->p = p;}
具体header解析如下, 有兴趣的点开瞅瞅:
// ziplist.c/* Decode the length of the previous element, from the perspective of the entry* pointed to by 'ptr'. */#define ZIP_DECODE_PREVLEN(ptr, prevlensize, prevlen) do { \// 解析第1个字符为 prevlensizeZIP_DECODE_PREVLENSIZE(ptr, prevlensize); \if ((prevlensize) == 1) { \(prevlen) = (ptr)[0]; \} else if ((prevlensize) == 5) { \assert(sizeof((prevlensize)) == 4); \// 当ptr[0]>254时,代表内容有点大,需要使用 5个字符保存上一字符长度memcpy(&(prevlen), ((char*)(ptr)) + 1, 4); \memrev32ifbe(&prevlen); \} \} while(0);/* Decode the number of bytes required to store the length of the previous* element, from the perspective of the entry pointed to by 'ptr'. */#define ZIP_DECODE_PREVLENSIZE(ptr, prevlensize) do { \if ((ptr)[0] < ZIP_BIGLEN) { \(prevlensize) = 1; \} else { \(prevlensize) = 5; \} \} while(0);/* Decode the length encoded in 'ptr'. The 'encoding' variable will hold the* entries encoding, the 'lensize' variable will hold the number of bytes* required to encode the entries length, and the 'len' variable will hold the* entries length. */#define ZIP_DECODE_LENGTH(ptr, encoding, lensize, len) do { \// 解析第1个字符为 编码格式 &ZIP_STR_MASK=0xc0ZIP_ENTRY_ENCODING((ptr), (encoding)); \if ((encoding) < ZIP_STR_MASK) { \// 0 << 6 =0// 具体解析如下代码,if ((encoding) == ZIP_STR_06B) { \(lensize) = 1; \(len) = (ptr)[0] & 0x3f; \}// 1 << 6 =64else if ((encoding) == ZIP_STR_14B) { \(lensize) = 2; \(len) = (((ptr)[0] & 0x3f) << 8) | (ptr)[1]; \}// 2 << 6 =128else if (encoding == ZIP_STR_32B) { \(lensize) = 5; \(len) = ((ptr)[1] << 24) | \((ptr)[2] << 16) | \((ptr)[3] << 8) | \((ptr)[4]); \} else { \assert(NULL); \} \} else { \// 超过 0xc0 的长度了,直接使用 1,2,3,4 表示len(lensize) = 1; \(len) = zipIntSize(encoding); \} \} while(0);/* Extract the encoding from the byte pointed by 'ptr' and set it into* 'encoding'. */#define ZIP_ENTRY_ENCODING(ptr, encoding) do { \(encoding) = (ptr[0]); \if ((encoding) < ZIP_STR_MASK) (encoding) &= ZIP_STR_MASK; \} while(0)/* Different encoding/length possibilities */#define ZIP_STR_MASK 0xc0#define ZIP_INT_MASK 0x30#define ZIP_STR_06B (0 << 6) // 0x00#define ZIP_STR_14B (1 << 6) // 0x40#define ZIP_STR_32B (2 << 6) // 0x80#define ZIP_INT_16B (0xc0 | 0<<4) // 0xc0#define ZIP_INT_32B (0xc0 | 1<<4) // 0xd0#define ZIP_INT_64B (0xc0 | 2<<4) // 0xe0#define ZIP_INT_24B (0xc0 | 3<<4) // 0xf0#define ZIP_INT_8B 0xfe // 0xfe
添加kv到对应的key实例中:
// 3. 添加kv到 hash表中, 稍微复杂// t_hash.c, 做变更到hash表中int hashTypeSet(robj *o, sds field, sds value, int flags) {int update = 0;// 针对ziplist 的添加, 与 ht 编码的添加, 自然是分别处理if (o->encoding == OBJ_ENCODING_ZIPLIST) {unsigned char *zl, *fptr, *vptr;zl = o->ptr;// 找到ziplist 的头节点指针fptr = ziplistIndex(zl, ZIPLIST_HEAD);if (fptr != NULL) {// 尝试查找该 field 对应的元素(从1开始),如果找到则先删除原值,然后统一添加fptr = ziplistFind(fptr, (unsigned char*)field, sdslen(field), 1);if (fptr != NULL) {/* Grab pointer to the value (fptr points to the field) */// value 不可以为null, 否则 ziplist 将无法工作vptr = ziplistNext(zl, fptr);serverAssert(vptr != NULL);update = 1;/* Delete value */// 先删除旧的 value, 再以插入的形式更新, 后续讲删除时再详解zl = ziplistDelete(zl, &vptr);/* Insert new value */// 重点,将value添加到 ziplist 中zl = ziplistInsert(zl, vptr, (unsigned char*)value,sdslen(value));}}// 没有找到对应元素,则直接将元素添加到尾部即可if (!update) {/* Push new field/value pair onto the tail of the ziplist */zl = ziplistPush(zl, (unsigned char*)field, sdslen(field),ZIPLIST_TAIL);zl = ziplistPush(zl, (unsigned char*)value, sdslen(value),ZIPLIST_TAIL);}o->ptr = zl;/* Check if the ziplist needs to be converted to a hash table */// 大于设置的阀值后,转换ziplist为ht(默认: 512)if (hashTypeLength(o) > server.hash_max_ziplist_entries)hashTypeConvert(o, OBJ_ENCODING_HT);} else if (o->encoding == OBJ_ENCODING_HT) {dictEntry *de = dictFind(o->ptr,field);if (de) {sdsfree(dictGetVal(de));if (flags & HASH_SET_TAKE_VALUE) {dictGetVal(de) = value;value = NULL;} else {dictGetVal(de) = sdsdup(value);}update = 1;} else {sds f,v;if (flags & HASH_SET_TAKE_FIELD) {f = field;field = NULL;} else {f = sdsdup(field);}if (flags & HASH_SET_TAKE_VALUE) {v = value;value = NULL;} else {v = sdsdup(value);}dictAdd(o->ptr,f,v);}} else {serverPanic("Unknown hash encoding");}/* Free SDS strings we did not referenced elsewhere if the flags* want this function to be responsible. */if (flags & HASH_SET_TAKE_FIELD && field) sdsfree(field);if (flags & HASH_SET_TAKE_VALUE && value) sdsfree(value);return update;}// 3.1. 使用ziplist进行保存 field -> value// ziplist.c, 查找某个 field 是否存在于ziplist中/* Find pointer to the entry equal to the specified entry. Skip 'skip' entries* between every comparison. Returns NULL when the field could not be found. */unsigned char *ziplistFind(unsigned char *p, unsigned char *vstr, unsigned int vlen, unsigned int skip) {int skipcnt = 0;unsigned char vencoding = 0;long long vll = 0;while (p[0] != ZIP_END) {unsigned int prevlensize, encoding, lensize, len;unsigned char *q;// 解析整个字符串p的 prevlensize,encoding,lensize,lenZIP_DECODE_PREVLENSIZE(p, prevlensize);ZIP_DECODE_LENGTH(p + prevlensize, encoding, lensize, len);q = p + prevlensize + lensize;// 传入1, 代表要跳过一个元素, 比如: 查找key时,跳过1个v,然后继续迭代// 跳过了n个元素后,再从此开始key的比对过程if (skipcnt == 0) {/* Compare current entry with specified entry */// 针对不同的编码使用不同的比较方式if (ZIP_IS_STR(encoding)) {// 找到相应的元素,直接返回 p 指针if (len == vlen && memcmp(q, vstr, vlen) == 0) {return p;}} else {/* Find out if the searched field can be encoded. Note that* we do it only the first time, once done vencoding is set* to non-zero and vll is set to the integer value. */if (vencoding == 0) {if (!zipTryEncoding(vstr, vlen, &vll, &vencoding)) {/* If the entry can't be encoded we set it to* UCHAR_MAX so that we don't retry again the next* time. */vencoding = UCHAR_MAX;}/* Must be non-zero by now */assert(vencoding);}/* Compare current entry with specified entry, do it only* if vencoding != UCHAR_MAX because if there is no encoding* possible for the field it can't be a valid integer. */if (vencoding != UCHAR_MAX) {long long ll = zipLoadInteger(q, encoding);if (ll == vll) {return p;}}}/* Reset skip count */// 查找一次,跳过skip次skipcnt = skip;} else {/* Skip entry */skipcnt--;}/* Move to next entry */p = q + len;}return NULL;}// ziplist.c, 添加value到ziplist中// zl:ziplist实例, p:要插入的key字串, s:要插入的value字串, len:要插入的value的长度/* Insert an entry at "p". */unsigned char *ziplistInsert(unsigned char *zl, unsigned char *p, unsigned char *s, unsigned int slen) {return __ziplistInsert(zl,p,s,slen);}/* Insert item at "p". */static unsigned char *__ziplistInsert(unsigned char *zl, unsigned char *p, unsigned char *s, unsigned int slen) {size_t curlen = intrev32ifbe(ZIPLIST_BYTES(zl)), reqlen;unsigned int prevlensize, prevlen = 0;size_t offset;int nextdiff = 0;unsigned char encoding = 0;long long value = 123456789; /* initialized to avoid warning. Using a valuethat is easy to see if for some reasonwe use it uninitialized. */zlentry tail;/* Find out prevlen for the entry that is inserted. */if (p[0] != ZIP_END) {ZIP_DECODE_PREVLEN(p, prevlensize, prevlen);} else {unsigned char *ptail = ZIPLIST_ENTRY_TAIL(zl);if (ptail[0] != ZIP_END) {prevlen = zipRawEntryLength(ptail);}}/* See if the entry can be encoded */if (zipTryEncoding(s,slen,&value,&encoding)) {/* 'encoding' is set to the appropriate integer encoding */reqlen = zipIntSize(encoding);} else {/* 'encoding' is untouched, however zipEncodeLength will use the* string length to figure out how to encode it. */reqlen = slen;}/* We need space for both the length of the previous entry and* the length of the payload. */// 加上prevlen,encoding,slen 的长度,以计算value的存放位置reqlen += zipPrevEncodeLength(NULL,prevlen);reqlen += zipEncodeLength(NULL,encoding,slen);/* When the insert position is not equal to the tail, we need to* make sure that the next entry can hold this entry's length in* its prevlen field. */nextdiff = (p[0] != ZIP_END) ? zipPrevLenByteDiff(p,reqlen) : 0;/* Store offset because a realloc may change the address of zl. */// 存储当前偏移位置,以便在扩容之后,还能找到相应位置// p = p -zl + zloffset = p-zl;zl = ziplistResize(zl,curlen+reqlen+nextdiff);p = zl+offset;/* Apply memory move when necessary and update tail offset. */if (p[0] != ZIP_END) {/* Subtract one because of the ZIP_END bytes */// 字符拷贝memmove(p+reqlen,p-nextdiff,curlen-offset-1+nextdiff);/* Encode this entry's raw length in the next entry. */zipPrevEncodeLength(p+reqlen,reqlen);/* Update offset for tail */ZIPLIST_TAIL_OFFSET(zl) =intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+reqlen);/* When the tail contains more than one entry, we need to take* "nextdiff" in account as well. Otherwise, a change in the* size of prevlen doesn't have an effect on the *tail* offset. */zipEntry(p+reqlen, &tail);if (p[reqlen+tail.headersize+tail.len] != ZIP_END) {ZIPLIST_TAIL_OFFSET(zl) =intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+nextdiff);}} else {/* This element will be the new tail. */ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(p-zl);}/* When nextdiff != 0, the raw length of the next entry has changed, so* we need to cascade the update throughout the ziplist */if (nextdiff != 0) {// 如果本次更新后数据位置变化,则需要更新后续的元素位置offset = p-zl;zl = __ziplistCascadeUpdate(zl,p+reqlen);p = zl+offset;}/* Write the entry */// 将 value 写入 p 中, 即写入了 ziplist 中p += zipPrevEncodeLength(p,prevlen);p += zipEncodeLength(p,encoding,slen);if (ZIP_IS_STR(encoding)) {memcpy(p,s,slen);} else {zipSaveInteger(p,value,encoding);}ZIPLIST_INCR_LENGTH(zl,1);return zl;}// 另外,如果没有旧的元素值时,直接在hash表的末尾添加对应的field->value 即可// ziplist.c, 在尾部进行添加元素,没有许多的情况要考虑,但是代码完全复用 __ziplistInsert()unsigned char *ziplistPush(unsigned char *zl, unsigned char *s, unsigned int slen, int where) {unsigned char *p;p = (where == ZIPLIST_HEAD) ? ZIPLIST_ENTRY_HEAD(zl) : ZIPLIST_ENTRY_END(zl);return __ziplistInsert(zl,p,s,slen);}
鉴于插入过程稍微复杂,咱们画个图重新理一下思路:

看起来没ziplist好像没那么简单呢,为啥还要搞这么复杂呢?其实以上代码,仅是在人看来复杂,对机器来说就是更多的移位计算操作,多消耗点cpu就换来了空间上的节省,是可以的。软件本身的复杂性带来了效益,是软件的价值体现,所以,并非所有的东西都是简单即美。
接下来,我们来看一下使用 HT 的编码又如何存储field->value呢?
// 3.2. OBJ_ENCODING_HT 的 field -> value 的添加if (o->encoding == OBJ_ENCODING_HT) {// hash 表中查找对应的 fielddictEntry *de = dictFind(o->ptr,field);if (de) {sdsfree(dictGetVal(de));// hset 时使用 HASH_SET_COPY, 所以直接使用 sdsdup() 即可if (flags & HASH_SET_TAKE_VALUE) {dictGetVal(de) = value;value = NULL;} else {dictGetVal(de) = sdsdup(value);}update = 1;} else {// 新增 field -> valuesds f,v;if (flags & HASH_SET_TAKE_FIELD) {f = field;field = NULL;} else {f = sdsdup(field);}if (flags & HASH_SET_TAKE_VALUE) {v = value;value = NULL;} else {v = sdsdup(value);}// 添加到 hash 表中,前些篇章讲解过,大概就是计算hash,放入v的过程dictAdd(o->ptr,f,v);}}
如此看来,OBJ_ENCODING_HT 的实现反而简单了哦。
总结下 hash的插入过程,hash 初始创建时都是使用ziplist 进行容纳元素的,在特定情况下会触发 ziplist 为 ht 的编码方式, 比如:
1. hset时自身的参数大于设置值(默认: 64)时直接转换 ziplist -> ht;
2. hash表的元素数量大于设置值(默认: 512)时转换 ziplist -> ht;
这么设计的原因是,元素较少且占用空间较小时,使用ziplist会节省空间,且时间消耗与hash表相关并不大,所以 ziplist 是优先的选择了。但是大量数据还是必须要使用hash表存储的。
二、hmset 批量添加元素
hset 和 hmset 在实现上基本如出一辙,所以简单瞅瞅就得了。
// t_hash.c, hmset key f1 v1 f2 v2void hmsetCommand(client *c) {int i;robj *o;// 参数个数检查,必定是2nif ((c->argc % 2) == 1) {addReplyError(c,"wrong number of arguments for HMSET");return;}// 插入方式与 hset 一毛一样,差别在于批量插入时,会循环向 key-hash表中添加field->valueif ((o = hashTypeLookupWriteOrCreate(c,c->argv[1])) == NULL) return;hashTypeTryConversion(o,c->argv,2,c->argc-1);// 循环insertfor (i = 2; i < c->argc; i += 2) {hashTypeSet(o,c->argv[i]->ptr,c->argv[i+1]->ptr,HASH_SET_COPY);}addReply(c, shared.ok);signalModifiedKey(c->db,c->argv[1]);notifyKeyspaceEvent(NOTIFY_HASH,"hset",c->argv[1],c->db->id);server.dirty++;}
三、hget 获取某字段值
这种命令的时间复杂度都是 O(1), 所以一般是简单至上。
// t_hash.cvoid hgetCommand(client *c) {robj *o;// 查找key, 不存在或者类型不一致则直接返回if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.nullbulk)) == NULL ||checkType(c,o,OBJ_HASH)) return;// 基于o, 返回 field 对应的元素值即可addHashFieldToReply(c, o, c->argv[2]->ptr);}// t_hash.cstatic void addHashFieldToReply(client *c, robj *o, sds field) {int ret;if (o == NULL) {addReply(c, shared.nullbulk);return;}if (o->encoding == OBJ_ENCODING_ZIPLIST) {unsigned char *vstr = NULL;unsigned int vlen = UINT_MAX;long long vll = LLONG_MAX;// 基于 ziplist,ret = hashTypeGetFromZiplist(o, field, &vstr, &vlen, &vll);if (ret < 0) {// 响应为空addReply(c, shared.nullbulk);} else {// 添加到输出缓冲if (vstr) {addReplyBulkCBuffer(c, vstr, vlen);} else {addReplyBulkLongLong(c, vll);}}} else if (o->encoding == OBJ_ENCODING_HT) {// hash 表类型则查找 hash 表即可sds value = hashTypeGetFromHashTable(o, field);// 添加到输出缓冲if (value == NULL)// 响应为空addReply(c, shared.nullbulk);elseaddReplyBulkCBuffer(c, value, sdslen(value));} else {serverPanic("Unknown hash encoding");}}// t_hash.c, 从 ziplist 中查找 field 值/* Get the value from a ziplist encoded hash, identified by field.* Returns -1 when the field cannot be found. */int hashTypeGetFromZiplist(robj *o, sds field,unsigned char **vstr,unsigned int *vlen,long long *vll){unsigned char *zl, *fptr = NULL, *vptr = NULL;int ret;serverAssert(o->encoding == OBJ_ENCODING_ZIPLIST);zl = o->ptr;fptr = ziplistIndex(zl, ZIPLIST_HEAD);if (fptr != NULL) {fptr = ziplistFind(fptr, (unsigned char*)field, sdslen(field), 1);if (fptr != NULL) {/* Grab pointer to the value (fptr points to the field) */vptr = ziplistNext(zl, fptr);serverAssert(vptr != NULL);}}if (vptr != NULL) {ret = ziplistGet(vptr, vstr, vlen, vll);serverAssert(ret);return 0;}return -1;}// t_hash.c, 从hash表中查找 field 字段的值/* Get the value from a hash table encoded hash, identified by field.* Returns NULL when the field cannot be found, otherwise the SDS value* is returned. */sds hashTypeGetFromHashTable(robj *o, sds field) {dictEntry *de;serverAssert(o->encoding == OBJ_ENCODING_HT);de = dictFind(o->ptr, field);if (de == NULL) return NULL;return dictGetVal(de);}
四、hmget 批量获取值
与hget如出一辙。
// t_hash.cvoid hmgetCommand(client *c) {robj *o;int i;/* Don't abort when the key cannot be found. Non-existing keys are empty* hashes, where HMGET should respond with a series of null bulks. */o = lookupKeyRead(c->db, c->argv[1]);if (o != NULL && o->type != OBJ_HASH) {addReply(c, shared.wrongtypeerr);return;}// 循环输出值addReplyMultiBulkLen(c, c->argc-2);for (i = 2; i < c->argc; i++) {addHashFieldToReply(c, o, c->argv[i]->ptr);}}
五、hgetall 获取所有hash的kv
hgetall 和 hmget 方式稍微有点不一样,原因是为了让 hkeysCommand/hvalsCommand 进行复用。
// t_hash.cvoid hgetallCommand(client *c) {genericHgetallCommand(c,OBJ_HASH_KEY|OBJ_HASH_VALUE);}void genericHgetallCommand(client *c, int flags) {robj *o;hashTypeIterator *hi;int multiplier = 0;int length, count = 0;if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.emptymultibulk)) == NULL|| checkType(c,o,OBJ_HASH)) return;if (flags & OBJ_HASH_KEY) multiplier++;if (flags & OBJ_HASH_VALUE) multiplier++;length = hashTypeLength(o) * multiplier;addReplyMultiBulkLen(c, length);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++;}}hashTypeReleaseIterator(hi);serverAssert(count == length);}static void addHashIteratorCursorToReply(client *c, hashTypeIterator *hi, int what) {if (hi->encoding == OBJ_ENCODING_ZIPLIST) {unsigned char *vstr = NULL;unsigned int vlen = UINT_MAX;long long vll = LLONG_MAX;hashTypeCurrentFromZiplist(hi, what, &vstr, &vlen, &vll);if (vstr)addReplyBulkCBuffer(c, vstr, vlen);elseaddReplyBulkLongLong(c, vll);} else if (hi->encoding == OBJ_ENCODING_HT) {sds value = hashTypeCurrentFromHashTable(hi, what);addReplyBulkCBuffer(c, value, sdslen(value));} else {serverPanic("Unknown hash encoding");}}
六、hincrby 增加x某字段
hincrby key field 1
// t_hash.c,void hincrbyCommand(client *c) {long long value, incr, oldvalue;robj *o;sds new;unsigned char *vstr;unsigned int vlen;// 解析增加字段值到 incr 中if (getLongLongFromObjectOrReply(c,c->argv[3],&incr,NULL) != C_OK) return;// 获取原值或者设置为0if ((o = hashTypeLookupWriteOrCreate(c,c->argv[1])) == NULL) return;if (hashTypeGetValue(o,c->argv[2]->ptr,&vstr,&vlen,&value) == C_OK) {if (vstr) {if (string2ll((char*)vstr,vlen,&value) == 0) {addReplyError(c,"hash value is not an integer");return;}} /* 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;}// 将相加后的值重置设置回hash表中value += incr;new = sdsfromlonglong(value);hashTypeSet(o,c->argv[2]->ptr,new,HASH_SET_TAKE_VALUE);addReplyLongLong(c,value);signalModifiedKey(c->db,c->argv[1]);notifyKeyspaceEvent(NOTIFY_HASH,"hincrby",c->argv[1],c->db->id);server.dirty++;}
七、hdel 删除某字段
hdel key field
// t_hash.c,void hdelCommand(client *c) {robj *o;int j, deleted = 0, keyremoved = 0;if ((o = lookupKeyWriteOrReply(c,c->argv[1],shared.czero)) == NULL ||checkType(c,o,OBJ_HASH)) return;// 循环删除给定字段列表for (j = 2; j < c->argc; j++) {if (hashTypeDelete(o,c->argv[j]->ptr)) {deleted++;// 当没有任何元素后,直接将key删除if (hashTypeLength(o) == 0) {dbDelete(c->db,c->argv[1]);keyremoved = 1;break;}}}if (deleted) {signalModifiedKey(c->db,c->argv[1]);notifyKeyspaceEvent(NOTIFY_HASH,"hdel",c->argv[1],c->db->id);if (keyremoved)notifyKeyspaceEvent(NOTIFY_GENERIC,"del",c->argv[1],c->db->id);server.dirty += deleted;}addReplyLongLong(c,deleted);}// 具体删除 field, 同样区分编码类型,不同处理逻辑/* Delete an element from a hash.* Return 1 on deleted and 0 on not found. */int hashTypeDelete(robj *o, sds field) {int deleted = 0;if (o->encoding == OBJ_ENCODING_ZIPLIST) {unsigned char *zl, *fptr;zl = o->ptr;fptr = ziplistIndex(zl, ZIPLIST_HEAD);if (fptr != NULL) {// ziplist 删除,依次删除 field, valuefptr = ziplistFind(fptr, (unsigned char*)field, sdslen(field), 1);if (fptr != NULL) {// ziplistDelete 为原地删除,所以只要调用2次,即把kv删除zl = ziplistDelete(zl,&fptr);zl = ziplistDelete(zl,&fptr);o->ptr = zl;deleted = 1;}}} else if (o->encoding == OBJ_ENCODING_HT) {if (dictDelete((dict*)o->ptr, field) == C_OK) {deleted = 1;/* Always check if the dictionary needs a resize after a delete. */// hash 删除的,可能需要进行缩容操作,这种处理方法相对特殊些if (htNeedsResize(o->ptr)) dictResize(o->ptr);}} else {serverPanic("Unknown hash encoding");}return deleted;}// server.c, 是否需要进行 resizeint htNeedsResize(dict *dict) {long long size, used;size = dictSlots(dict);used = dictSize(dict);// HASHTABLE_MIN_FILL=10, 即使用率小于 1/10 时,可以进行缩容操作了return (size && used && size > DICT_HT_INITIAL_SIZE &&(used*100/size < HASHTABLE_MIN_FILL));}
至此,整个hash数据结构的解析算是完整了。总体来说,hash由两种数据结构承载,ziplist在小数据量时使用,稍微复杂,但对于昂贵的内存来说是值得的。hash表在数据量大时使用,容易理解。通过本文的讲解,相信可以验证了你对redis hash 的实现的猜想了。

腾讯、阿里、滴滴后台面试题汇总总结 — (含答案)
面试:史上最全多线程面试题 !
最新阿里内推Java后端面试题
JVM难学?那是因为你没认真看完这篇文章

关注作者微信公众号 —《JAVA烂猪皮》
了解更多java后端架构知识以及最新面试宝典


看完本文记得给作者点赞+在看哦~~~大家的支持,是作者源源不断出文的动力
作者:等你归去来
出处:https://www.cnblogs.com/yougewe/p/12234983.html
