五: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
2. 如果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

    扫描二维码关注公众号,回复: 10315829 查看本文章
  • 用法: 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,返回
2. 计算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

发布了121 篇原创文章 · 获赞 56 · 访问量 167万+

猜你喜欢

转载自blog.csdn.net/u013565163/article/details/104995770