五:redis的hash数据类型常用命令、场景、源码分析
五:redis的hash数据类型常用命令、场景、源码分析
哈希数据类型存储key在field和value中,如果存储一个用户的信息key是user:id,field为nickname,age,sex,value存储对应的值,可以快速查找用户信息的某个属性。
命令
hset
- 解释:设置/更新hash类型的键值对
- 用法: hset key field value
本文分析的源码是基于redis2.2,只支持单个field/value的设置
redis4.0.0开始支持设置多对field/value设置/更新 示例
127.0.0.1:6379> hset key1 field1 value1
ok
127.0.0.1:6379> hget key1 field1
“value1”源码分析
1.查找键是否存在并且是否是hash类型:如果键不存在则创建键,如果存在并且是hash类型返回对象,否则返回null
2.如果对象的编码是zipmap,并且field或value长度超过了64,则转换为hashtable
3.设置/更新file/value,如果当前的内部编码是zipmap,则插入/更新,如果zipmap的元素个数大于512则zipmap转为hashtable;如果当前的内部编码是hashtable,则进行相应的插入/更新操作
/** ** redis.c ** hset的定义 **/
struct redisCommand readonlyCommandTable[] = {
{ "hset",hsetCommand,4,REDIS_CMD_DENYOOM,NULL,1,1,1}
}
/** ** t_hash.c **/
void hsetCommand(redisClient *c) {
int update;
robj *o;
//1. 查找键是否存在并且是否是hash类型:如果键不存在则创建键,如果存在并且是hash类型返回对象,否则返回null
if ((o = hashTypeLookupWriteOrCreate(c,c->argv[1])) == NULL) return;
//2. 如果对象的编码是zipmap,并且field或value长度超过了64,则转换为hashtable
hashTypeTryConversion(o,c->argv,2,3);
hashTypeTryObjectEncoding(o,&c->argv[2], &c->argv[3]);
//3. 设置/更新file/value,如果当前的内部编码是zipmap,则插入/更新;
//如果zipmap的元素个数大于512则zipmap转为hashtable, 如果当前的内部编码是hashtable,则进行相应的插入/更新操作
update = hashTypeSet(o,c->argv[2],c->argv[3]);
addReply(c, update ? shared.czero : shared.cone);
touchWatchedKey(c->db,c->argv[1]);
server.dirty++;
}
1.在db的dict中查找键
2.没有找到创建键的内存
3.加入新创建的键的对象到db的dict中
4.如果键已存在,但不是hash类型,返回类型不匹配信息,方法返回null
/** ** t_hash.c ** 步骤1. 查找键是否存在并且是否是hash类型:如果键不存在则创建键,如果存在并且是hash类型返回对象,否则返回null **/
robj *hashTypeLookupWriteOrCreate(redisClient *c, robj *key) {
//1.在db的dict中查找键
robj *o = lookupKeyWrite(c->db,key);
if (o == NULL) {
//2.没有找到创建键的内存
o = createHashObject();
//3.加入新创建的键的对象到db的dict中
dbAdd(c->db,key,o);
} else {
if (o->type != REDIS_HASH) {
// 4.如果键已存在,但不是hash类型,返回类型不匹配信息,方法返回null
addReply(c,shared.wrongtypeerr);
return NULL;
}
}
return o;
}
1.创建键的内部编码 zipmap空对象,file/value会写入到这对象中
2. 创建键的对象
3. 设置键的内部编码为zipmap
/** ** object.c ** 步骤1 **/
robj *createHashObject(void) {
/* All the Hashes start as zipmaps. Will be automatically converted * into hash tables if there are enough elements or big elements * inside. */
//1.创建键的内部编码 zipmap空对象,file/value会写入到这对象中
unsigned char *zm = zipmapNew();
//2. 创建键的对象
robj *o = createObject(REDIS_HASH,zm);
//3. 设置键的内部编码为zipmap
o->encoding = REDIS_ENCODING_ZIPMAP;
return o;
}
1.内存分配两个字节
2.初始化指定zipmap的长度为0, 当前还没有写入field/value
3.zipmap的结束标志 ZIPMAP_END= 255
unsigned char *zipmapNew(void) {
//1.内存分配两个字节
unsigned char *zm = zmalloc(2);
//2.初始化指定zipmap的长度为0, 当前还没有写入field/value
zm[0] = 0; /* Length */
//3.zipmap的结束标志 ZIPMAP_END= 255
zm[1] = ZIPMAP_END;
return zm;
}
1.设置数据类型为hash
2.对象的内部编码默认为字符,后面会被设置为zipmap
3.无类型指针指向 前面创建的zm zipmap
4. 引用计数记为1
/** ** object.c ** 步骤1 **/
robj *createObject(int type, void *ptr) {
robj *o = zmalloc(sizeof(*o));
//1.设置数据类型为hash
o->type = type;
//2.对象的内部编码默认为字符,后面会被设置为zipmap
o->encoding = REDIS_ENCODING_RAW;
//3.无类型指针指向 前面创建的zm zipmap
o->ptr = ptr;
//4. 引用计数记为1
o->refcount = 1;
/* Set the LRU to the current lruclock (minutes resolution). * We do this regardless of the fact VM is active as LRU is also * used for the maxmemory directive when Redis is used as cache. * * Note that this code may run in the context of an I/O thread * and accessing server.lruclock in theory is an error * (no locks). But in practice this is safe, and even if we read * garbage Redis will not fail. */
o->lru = server.lruclock;
/* The following is only needed if VM is active, but since the conditional * is probably more costly than initializing the field it's better to * have every field properly initialized anyway. */
o->storage = REDIS_VM_MEMORY;
return o;
}
1.zipmap设置/更新filed/value
如果zipmap的元素个数大于512则zipmap转为hashtable
/ t_hash.c 步骤3 /
int hashTypeSet(robj o, robj key, robj *value) {
int update = 0;
if (o->encoding == REDIS_ENCODING_ZIPMAP) {
key = getDecodedObject(key);
value = getDecodedObject(value);
//1.zipmap设置/更新filed/value
o->ptr = zipmapSet(o->ptr,
key->ptr,sdslen(key->ptr),
value->ptr,sdslen(value->ptr), &update);
decrRefCount(key);
decrRefCount(value);
/* Check if the zipmap needs to be upgraded to a real hash table */
// 2.如果zipmap的元素个数大于512则zipmap转为hashtable
if (zipmapLen(o->ptr) > server.hash_max_zipmap_entries)
convertToRealHash(o);
} else {
if (dictReplace(o->ptr,key,value)) {
/* Insert */
incrRefCount(key);
} else {
/* Update */
update = 1;
}
incrRefCount(value);
}
return update;
}
hget
- 解释: 查询hash键field对应的值
- 用法: hget key field
示例:
127.0.0.1:6379> hget key1 field1
(nil)
127.0.0.1:6379> hset key1 field1 value1
OK
127.0.0.1:6379> hget key1 field1
“value1”源码分析
/ redis.c /
struct redisCommand readonlyCommandTable[] = {
{ "hget",hgetCommand,3,0,NULL,1,1,1}
}
1.如果键不存在或键的数据类型不是hash,则直接返回
2.返回field的值,如果内部编码为zipmap,则v为返回的值;如果为hashtable则返回值为value
3.返回给客户端值
3.1 返回内部编码为hash的field的值
3.2返回内部编码为zipmap的field的值
3.3没有field
/** ** t_hash.c **/
void hgetCommand(redisClient *c) {
robj *o, *value;
unsigned char *v;
unsigned int vlen;
int encoding;
//1.如果键不存在或键的数据类型不是hash,则直接返回
if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.nullbulk)) == NULL ||
checkType(c,o,REDIS_HASH)) return;
//2. 返回field的值,如果内部编码为zipmap,则v为返回的值;如果为hashtable则返回值为value
if ((encoding = hashTypeGet(o,c->argv[2],&value,&v,&vlen)) != -1) {
if (encoding == REDIS_ENCODING_HT)
//3.返回内部编码为hash的field的值
addReplyBulk(c,value);
else
//4.返回内部编码为zipmap的field的值
addReplyBulkCBuffer(c,v,vlen);
} else {
//5.没有field
addReply(c,shared.nullbulk);
}
}
int hashTypeGet(robj *o, robj *key, robj **objval, unsigned char **v,
unsigned int *vlen)
{
//内部编码为zipmap,在zipmap中查找
if (o->encoding == REDIS_ENCODING_ZIPMAP) {
int found;
key = getDecodedObject(key);
found = zipmapGet(o->ptr,key->ptr,sdslen(key->ptr),v,vlen);
decrRefCount(key);
if (!found) return -1;
} else {
//内部编码为hashtable,在hashtable中查找field
dictEntry *de = dictFind(o->ptr,key);
if (de == NULL) return -1;
*objval = dictGetEntryVal(de);
}
return o->encoding;
}
hdel
- 解释: 删除hash键的field,如果成功返回1,不存在则返回0
- 用法: hdel key field
示例
127.0.0.1:6379> hdel key1 field1
(integer)0
127.0.0.1:6379> hset key1 field1 value1
OK
127.0.0.1:6379> hdel key1 field1
(integer)1源码分析
1.如果键不存在或者键的数据类型不是hash,返回
2.删除键的field
3.删除成功后,如果键的长度为0,则删除键
4.field不存在返回0
/** ** redis.c ** **/
struct redisCommand readonlyCommandTable[] = {
{ "hdel",hdelCommand,3,0,NULL,1,1,1}
}
void hdelCommand(redisClient *c) {
robj *o;
//1.如果键不存在或者键的数据类型不是hash,返回
if ((o = lookupKeyWriteOrReply(c,c->argv[1],shared.czero)) == NULL ||
checkType(c,o,REDIS_HASH)) return;
//2.删除键的field
if (hashTypeDelete(o,c->argv[2])) {
//3.删除成功后,如果键的长度为0,则删除键
if (hashTypeLength(o) == 0) dbDelete(c->db,c->argv[1]);
addReply(c,shared.cone);
touchWatchedKey(c->db,c->argv[1]);
server.dirty++;
} else {
//4.field不存在返回0
addReply(c,shared.czero);
}
}
1.deleted初始化0表示在键中未发现field
2.键的内部编码为zipmap,在zipmap中删除field
3.键的内部编码为hashtable,在hashtable中删除field
/** ** t_hash.c ** 步骤2 **/
int hashTypeDelete(robj *o, robj *key) {
//1.deleted初始化0表示在键中未发现field
int deleted = 0;
if (o->encoding == REDIS_ENCODING_ZIPMAP) {
key = getDecodedObject(key);
//2.键的内部编码为zipmap,在zipmap中删除field
o->ptr = zipmapDel(o->ptr,key->ptr,sdslen(key->ptr), &deleted);
decrRefCount(key);
} else {
//3.键的内部编码为hashtable,在hashtable中删除field
deleted = dictDelete((dict*)o->ptr,key) == DICT_OK;
/* Always check if the dictionary needs a resize after a delete. */
if (deleted && htNeedsResize(o->ptr)) dictResize(o->ptr);
}
return deleted;
}
hgetall
- 解释:返回hash键的所有field和对应的value
- 用法: hgetall key
示例:
127.0.0.1:6379> hgetall key
(error)WRONGTYPE Operation against a key holding the wrong kind of value
127.0.0.1:6379> hset key1 field1 value1
OK
127.0.0.1:6379> hset key1 field2 value2
OK
127.0.0.1:6379> hgetall key1
1)”field1”
2)”value1”
3)”field2”
4)”value2”
127.0.0.1:6379> hdel key1 field1
(integer)1
127.0.0.1:6379> hdel key1 field2
(integer)1
127.0.0.1:6379> hgetall key1
(empty list or set)源码分析
1.如果键不存在或者键的数据类型不是hash,返回
2.创建迭代器
3.返回field的值
4.返回value的值
5.关闭迭代器
/** ** redis.c ** **/
struct redisCommand readonlyCommandTable[] = {
{ "hgetall",hgetallCommand,2,0,NULL,1,1,1}
}
void hgetallCommand(redisClient *c) {
genericHgetallCommand(c,REDIS_HASH_KEY|REDIS_HASH_VALUE);
}
void genericHgetallCommand(redisClient *c, int flags) {
robj *o;
unsigned long count = 0;
hashTypeIterator *hi;
void *replylen = NULL;
//1.如果键不存在或者键的数据类型不是hash,返回
if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.emptymultibulk)) == NULL
|| checkType(c,o,REDIS_HASH)) return;
replylen = addDeferredMultiBulkLength(c);
//2.创建迭代器
hi = hashTypeInitIterator(o);
while (hashTypeNext(hi) != REDIS_ERR) {
robj *obj;
unsigned char *v = NULL;
unsigned int vlen = 0;
int encoding;
//3.返回field的值
if (flags & REDIS_HASH_KEY) {
encoding = hashTypeCurrent(hi,REDIS_HASH_KEY,&obj,&v,&vlen);
if (encoding == REDIS_ENCODING_HT)
addReplyBulk(c,obj);
else
addReplyBulkCBuffer(c,v,vlen);
count++;
}
//4. 返回value的值
if (flags & REDIS_HASH_VALUE) {
encoding = hashTypeCurrent(hi,REDIS_HASH_VALUE,&obj,&v,&vlen);
if (encoding == REDIS_ENCODING_HT)
addReplyBulk(c,obj);
else
addReplyBulkCBuffer(c,v,vlen);
count++;
}
}
//5. 关闭迭代器
hashTypeReleaseIterator(hi);
setDeferredMultiBulkLength(c,replylen,count);
}
hincrby
- 解释: 指定hash键的数字类型的field的值增加或减少,返回最终的值
- 用法: hincrby key field value
示例:
127.0.0.1:6379>hset key1 field1 1
ok
127.0.0.1:6379>hset key1 field2 value
ok
127.0.0.1:6379>hincrby key1 field1 3
(integer) 4
127.0.0.1:6379>hincrby key1 field2 1
(error) Error hash value is not a integer
127.0.0.1:6379> hincrby key1 field3 2
(integer) 2
hkeys
- 解释:返回键的所有field
- 用法: hkeys key
示例
127.0.0.1:6379> hset key1 field1 value1
127.0.0.1:6379> hset key1 field2 value2
127.0.0.1:6379> hkeys key1
1)”field1”
2)”field2”源码分析
源码见hgetall命令
/** ** redis.c ** **/
struct redisCommand readonlyCommandTable[] = {
{ "hkeys",hkeysCommand,2,0,NULL,1,1,1}
}
void hkeysCommand(redisClient *c) {
genericHgetallCommand(c,REDIS_HASH_KEY);
}
hvals
- 解释:返回hash键的所有的field的值
- 用法: hvals key
示例:
127.0.0.1:6379> hset key1 field1 value1
127.0.0.1:6379> hset key1 field2 value2
127.0.0.1:6379> hvals key1
1)”value1”
2)”value2”源码分析
源码见hgetall命令
/** ** redis.c ** **/
struct redisCommand readonlyCommandTable[] = {
{ "hvals",hvalsCommand,2,0,NULL,1,1,1}
}
void hvalsCommand(redisClient *c) {
genericHgetallCommand(c,REDIS_HASH_VALUE);
}
hlen
- 解释: 返回hash键的field的数量
- 用法: hlen key
示例:
127.0.0.1:6379> hset key1 field1 value1
ok
127.0.0.1:6379> hset key1 field2 value2
ok
127.0.0.1:6379> hlen key1
(integer)2源码分析
1.如果键不存在或者键的数据类型不是hash,返回
计算hash键中field的数量
/ redis.c /
struct redisCommand readonlyCommandTable[] = {
{ "hlen",hlenCommand,2,0,NULL,1,1,1}
}
void hlenCommand(redisClient *c) {
robj *o;
//1.如果键不存在或者键的数据类型不是hash,返回
if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.czero)) == NULL ||
checkType(c,o,REDIS_HASH)) return;
//2. 计算hash键中field的数量
addReplyLongLong(c,hashTypeLength(o));
}
unsigned long hashTypeLength(robj *o) {
// 如果是键的内部编码是zipmap计算zipmap中键的个数,否则为hashtbale计算键的个数
return (o->encoding == REDIS_ENCODING_ZIPMAP) ?
zipmapLen((unsigned char*)o->ptr) : dictSize((dict*)o->ptr);
}
hexists
- 解释: field是否存在于hash键中,返回1表示存在,0表示不存在
- 用法: hexists key field
示例
127.0.0.1:6379> hexists key1 field1
(integer) 0
127.0.0.1:6379> hset key1 field1 value
ok
127.0.0.1:6379> hexists key1 field1
(integer)1
内部编码
本文基于redis2.2源码,hash类型的键的内部编码为:
- zipmap
- hashtable
使用场景
当缓存对象如个人信息时,如果使用字符类型存储,每次查询,修改某个属性都要序列化,
效率太低,此时可以使用hash
还没有评论,来说两句吧...