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 structzlentry {
unsignedintprevrawlensize, prevrawlen;
unsignedintlensize, len;
unsignedintheadersize;
unsignedcharencoding;
unsignedchar *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 structdict {
dictType*type;void *privdata;
dictht ht[2];long rehashidx; /*rehashing not in progress if rehashidx == -1*/unsignedlong iterators; /*number of iterators currently running*/} dict;
typedefstructdictht {
dictEntry**table;
unsignedlongsize;
unsignedlongsizemask;
unsignedlongused;
} dictht;
typedefstructdictEntry {void *key;void *val;struct dictEntry *next;
} dictEntry;
一、hset 设置单个 field -> value
“增删改查”中的“增改” 就是它了。
//t_hash.c, set key field value
void hsetCommand(client *c) {intupdate;
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.c
robj *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);returnNULL;
}
}returno;
}//object.c, 创建hashObject, 以 ziplist 形式创建
robj *createHashObject(void) {
unsignedchar *zl =ziplistNew();
robj*o =createObject(OBJ_HASH, zl);
o->encoding =OBJ_ENCODING_ZIPLIST;returno;
}//ziplist.c
static unsigned char *createList() {
unsignedchar *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);returnzl;
}//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, intend) {inti;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, intenc) {if (o->encoding ==OBJ_ENCODING_ZIPLIST) {//此处我们只处理这种情况
ha