五:redis的hash数据类型常用命令、场景、源码分析

矫情吗;* 2023-07-17 09:45 40阅读 0赞

五: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,则进行相应的插入/更新操作

  1. /** ** redis.c ** hset的定义 **/
  2. struct redisCommand readonlyCommandTable[] = {
  3. { "hset",hsetCommand,4,REDIS_CMD_DENYOOM,NULL,1,1,1}
  4. }
  5. /** ** t_hash.c **/
  6. void hsetCommand(redisClient *c) {
  7. int update;
  8. robj *o;
  9. //1. 查找键是否存在并且是否是hash类型:如果键不存在则创建键,如果存在并且是hash类型返回对象,否则返回null
  10. if ((o = hashTypeLookupWriteOrCreate(c,c->argv[1])) == NULL) return;
  11. //2. 如果对象的编码是zipmap,并且field或value长度超过了64,则转换为hashtable
  12. hashTypeTryConversion(o,c->argv,2,3);
  13. hashTypeTryObjectEncoding(o,&c->argv[2], &c->argv[3]);
  14. //3. 设置/更新file/value,如果当前的内部编码是zipmap,则插入/更新;
  15. //如果zipmap的元素个数大于512则zipmap转为hashtable, 如果当前的内部编码是hashtable,则进行相应的插入/更新操作
  16. update = hashTypeSet(o,c->argv[2],c->argv[3]);
  17. addReply(c, update ? shared.czero : shared.cone);
  18. touchWatchedKey(c->db,c->argv[1]);
  19. server.dirty++;
  20. }

1.在db的dict中查找键
2.没有找到创建键的内存
3.加入新创建的键的对象到db的dict中
4.如果键已存在,但不是hash类型,返回类型不匹配信息,方法返回null

  1. /** ** t_hash.c ** 步骤1. 查找键是否存在并且是否是hash类型:如果键不存在则创建键,如果存在并且是hash类型返回对象,否则返回null **/
  2. robj *hashTypeLookupWriteOrCreate(redisClient *c, robj *key) {
  3. //1.在db的dict中查找键
  4. robj *o = lookupKeyWrite(c->db,key);
  5. if (o == NULL) {
  6. //2.没有找到创建键的内存
  7. o = createHashObject();
  8. //3.加入新创建的键的对象到db的dict中
  9. dbAdd(c->db,key,o);
  10. } else {
  11. if (o->type != REDIS_HASH) {
  12. // 4.如果键已存在,但不是hash类型,返回类型不匹配信息,方法返回null
  13. addReply(c,shared.wrongtypeerr);
  14. return NULL;
  15. }
  16. }
  17. return o;
  18. }
  19. 1.创建键的内部编码 zipmap空对象,file/value会写入到这对象中
  20. 2. 创建键的对象
  21. 3. 设置键的内部编码为zipmap
  22. /** ** object.c ** 步骤1 **/
  23. robj *createHashObject(void) {
  24. /* All the Hashes start as zipmaps. Will be automatically converted * into hash tables if there are enough elements or big elements * inside. */
  25. //1.创建键的内部编码 zipmap空对象,file/value会写入到这对象中
  26. unsigned char *zm = zipmapNew();
  27. //2. 创建键的对象
  28. robj *o = createObject(REDIS_HASH,zm);
  29. //3. 设置键的内部编码为zipmap
  30. o->encoding = REDIS_ENCODING_ZIPMAP;
  31. return o;
  32. }
  33. 1.内存分配两个字节
  34. 2.初始化指定zipmap的长度为0, 当前还没有写入field/value
  35. 3.zipmap的结束标志 ZIPMAP_END= 255
  36. unsigned char *zipmapNew(void) {
  37. //1.内存分配两个字节
  38. unsigned char *zm = zmalloc(2);
  39. //2.初始化指定zipmap的长度为0, 当前还没有写入field/value
  40. zm[0] = 0; /* Length */
  41. //3.zipmap的结束标志 ZIPMAP_END= 255
  42. zm[1] = ZIPMAP_END;
  43. return zm;
  44. }
  45. 1.设置数据类型为hash
  46. 2.对象的内部编码默认为字符,后面会被设置为zipmap
  47. 3.无类型指针指向 前面创建的zm zipmap
  48. 4. 引用计数记为1
  49. /** ** object.c ** 步骤1 **/
  50. robj *createObject(int type, void *ptr) {
  51. robj *o = zmalloc(sizeof(*o));
  52. //1.设置数据类型为hash
  53. o->type = type;
  54. //2.对象的内部编码默认为字符,后面会被设置为zipmap
  55. o->encoding = REDIS_ENCODING_RAW;
  56. //3.无类型指针指向 前面创建的zm zipmap
  57. o->ptr = ptr;
  58. //4. 引用计数记为1
  59. o->refcount = 1;
  60. /* 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. */
  61. o->lru = server.lruclock;
  62. /* 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. */
  63. o->storage = REDIS_VM_MEMORY;
  64. return o;
  65. }

1.zipmap设置/更新filed/value

  1. 如果zipmap的元素个数大于512则zipmap转为hashtable

    / t_hash.c 步骤3 /

    int hashTypeSet(robj o, robj key, robj *value) {

    1. int update = 0;
    2. if (o->encoding == REDIS_ENCODING_ZIPMAP) {
    3. key = getDecodedObject(key);
    4. value = getDecodedObject(value);
    5. //1.zipmap设置/更新filed/value
    6. o->ptr = zipmapSet(o->ptr,
    7. key->ptr,sdslen(key->ptr),
    8. value->ptr,sdslen(value->ptr), &update);
    9. decrRefCount(key);
    10. decrRefCount(value);
    11. /* Check if the zipmap needs to be upgraded to a real hash table */
    12. // 2.如果zipmap的元素个数大于512则zipmap转为hashtable
    13. if (zipmapLen(o->ptr) > server.hash_max_zipmap_entries)
    14. convertToRealHash(o);
    15. } else {
    16. if (dictReplace(o->ptr,key,value)) {
    17. /* Insert */
    18. incrRefCount(key);
    19. } else {
    20. /* Update */
    21. update = 1;
    22. }
    23. incrRefCount(value);
    24. }
    25. 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[] = {

    1. { "hget",hgetCommand,3,0,NULL,1,1,1}

    }

  1. 1.如果键不存在或键的数据类型不是hash,则直接返回
  2. 2.返回field的值,如果内部编码为zipmap,则v为返回的值;如果为hashtable则返回值为value
  3. 3.返回给客户端值
  4. 3.1 返回内部编码为hashfield的值
  5. 3.2返回内部编码为zipmapfield的值
  6. 3.3没有field
  7. /** ** t_hash.c **/
  8. void hgetCommand(redisClient *c) {
  9. robj *o, *value;
  10. unsigned char *v;
  11. unsigned int vlen;
  12. int encoding;
  13. //1.如果键不存在或键的数据类型不是hash,则直接返回
  14. if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.nullbulk)) == NULL ||
  15. checkType(c,o,REDIS_HASH)) return;
  16. //2. 返回field的值,如果内部编码为zipmap,则v为返回的值;如果为hashtable则返回值为value
  17. if ((encoding = hashTypeGet(o,c->argv[2],&value,&v,&vlen)) != -1) {
  18. if (encoding == REDIS_ENCODING_HT)
  19. //3.返回内部编码为hash的field的值
  20. addReplyBulk(c,value);
  21. else
  22. //4.返回内部编码为zipmap的field的值
  23. addReplyBulkCBuffer(c,v,vlen);
  24. } else {
  25. //5.没有field
  26. addReply(c,shared.nullbulk);
  27. }
  28. }
  29. int hashTypeGet(robj *o, robj *key, robj **objval, unsigned char **v,
  30. unsigned int *vlen)
  31. {
  32. //内部编码为zipmap,在zipmap中查找
  33. if (o->encoding == REDIS_ENCODING_ZIPMAP) {
  34. int found;
  35. key = getDecodedObject(key);
  36. found = zipmapGet(o->ptr,key->ptr,sdslen(key->ptr),v,vlen);
  37. decrRefCount(key);
  38. if (!found) return -1;
  39. } else {
  40. //内部编码为hashtable,在hashtable中查找field
  41. dictEntry *de = dictFind(o->ptr,key);
  42. if (de == NULL) return -1;
  43. *objval = dictGetEntryVal(de);
  44. }
  45. return o->encoding;
  46. }

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

  1. /** ** redis.c ** **/
  2. struct redisCommand readonlyCommandTable[] = {
  3. { "hdel",hdelCommand,3,0,NULL,1,1,1}
  4. }
  5. void hdelCommand(redisClient *c) {
  6. robj *o;
  7. //1.如果键不存在或者键的数据类型不是hash,返回
  8. if ((o = lookupKeyWriteOrReply(c,c->argv[1],shared.czero)) == NULL ||
  9. checkType(c,o,REDIS_HASH)) return;
  10. //2.删除键的field
  11. if (hashTypeDelete(o,c->argv[2])) {
  12. //3.删除成功后,如果键的长度为0,则删除键
  13. if (hashTypeLength(o) == 0) dbDelete(c->db,c->argv[1]);
  14. addReply(c,shared.cone);
  15. touchWatchedKey(c->db,c->argv[1]);
  16. server.dirty++;
  17. } else {
  18. //4.field不存在返回0
  19. addReply(c,shared.czero);
  20. }
  21. }

1.deleted初始化0表示在键中未发现field
2.键的内部编码为zipmap,在zipmap中删除field
3.键的内部编码为hashtable,在hashtable中删除field

  1. /** ** t_hash.c ** 步骤2 **/
  2. int hashTypeDelete(robj *o, robj *key) {
  3. //1.deleted初始化0表示在键中未发现field
  4. int deleted = 0;
  5. if (o->encoding == REDIS_ENCODING_ZIPMAP) {
  6. key = getDecodedObject(key);
  7. //2.键的内部编码为zipmap,在zipmap中删除field
  8. o->ptr = zipmapDel(o->ptr,key->ptr,sdslen(key->ptr), &deleted);
  9. decrRefCount(key);
  10. } else {
  11. //3.键的内部编码为hashtable,在hashtable中删除field
  12. deleted = dictDelete((dict*)o->ptr,key) == DICT_OK;
  13. /* Always check if the dictionary needs a resize after a delete. */
  14. if (deleted && htNeedsResize(o->ptr)) dictResize(o->ptr);
  15. }
  16. return deleted;
  17. }

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.关闭迭代器

  1. /** ** redis.c ** **/
  2. struct redisCommand readonlyCommandTable[] = {
  3. { "hgetall",hgetallCommand,2,0,NULL,1,1,1}
  4. }
  5. void hgetallCommand(redisClient *c) {
  6. genericHgetallCommand(c,REDIS_HASH_KEY|REDIS_HASH_VALUE);
  7. }
  8. void genericHgetallCommand(redisClient *c, int flags) {
  9. robj *o;
  10. unsigned long count = 0;
  11. hashTypeIterator *hi;
  12. void *replylen = NULL;
  13. //1.如果键不存在或者键的数据类型不是hash,返回
  14. if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.emptymultibulk)) == NULL
  15. || checkType(c,o,REDIS_HASH)) return;
  16. replylen = addDeferredMultiBulkLength(c);
  17. //2.创建迭代器
  18. hi = hashTypeInitIterator(o);
  19. while (hashTypeNext(hi) != REDIS_ERR) {
  20. robj *obj;
  21. unsigned char *v = NULL;
  22. unsigned int vlen = 0;
  23. int encoding;
  24. //3.返回field的值
  25. if (flags & REDIS_HASH_KEY) {
  26. encoding = hashTypeCurrent(hi,REDIS_HASH_KEY,&obj,&v,&vlen);
  27. if (encoding == REDIS_ENCODING_HT)
  28. addReplyBulk(c,obj);
  29. else
  30. addReplyBulkCBuffer(c,v,vlen);
  31. count++;
  32. }
  33. //4. 返回value的值
  34. if (flags & REDIS_HASH_VALUE) {
  35. encoding = hashTypeCurrent(hi,REDIS_HASH_VALUE,&obj,&v,&vlen);
  36. if (encoding == REDIS_ENCODING_HT)
  37. addReplyBulk(c,obj);
  38. else
  39. addReplyBulkCBuffer(c,v,vlen);
  40. count++;
  41. }
  42. }
  43. //5. 关闭迭代器
  44. hashTypeReleaseIterator(hi);
  45. setDeferredMultiBulkLength(c,replylen,count);
  46. }

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命令

  1. /** ** redis.c ** **/
  2. struct redisCommand readonlyCommandTable[] = {
  3. { "hkeys",hkeysCommand,2,0,NULL,1,1,1}
  4. }
  5. void hkeysCommand(redisClient *c) {
  6. genericHgetallCommand(c,REDIS_HASH_KEY);
  7. }

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命令

  1. /** ** redis.c ** **/
  2. struct redisCommand readonlyCommandTable[] = {
  3. { "hvals",hvalsCommand,2,0,NULL,1,1,1}
  4. }
  5. void hvalsCommand(redisClient *c) {
  6. genericHgetallCommand(c,REDIS_HASH_VALUE);
  7. }

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,返回

  1. 计算hash键中field的数量

    / redis.c /

    struct redisCommand readonlyCommandTable[] = {

    1. { "hlen",hlenCommand,2,0,NULL,1,1,1}

    }

  1. void hlenCommand(redisClient *c) {
  2. robj *o;
  3. //1.如果键不存在或者键的数据类型不是hash,返回
  4. if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.czero)) == NULL ||
  5. checkType(c,o,REDIS_HASH)) return;
  6. //2. 计算hash键中field的数量
  7. addReplyLongLong(c,hashTypeLength(o));
  8. }
  9. unsigned long hashTypeLength(robj *o) {
  10. // 如果是键的内部编码是zipmap计算zipmap中键的个数,否则为hashtbale计算键的个数
  11. return (o->encoding == REDIS_ENCODING_ZIPMAP) ?
  12. zipmapLen((unsigned char*)o->ptr) : dictSize((dict*)o->ptr);
  13. }

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

发表评论

表情:
评论列表 (有 0 条评论,40人围观)

还没有评论,来说两句吧...

相关阅读