Redis 04_Redis哈希Hashes

一、Redis中哈希Hashes数据类型简介

1.1 哈希(Hashes)简介

Redis 的 哈希表(Hashes) 是一个 Strings 类型的 field 和 value 的映射表,Hashes 特别适合用于存储对象、映射关系和键值对等数据。Redis 中每个 Hashes 可以存储 2^32^-1 键值对(40 多亿)。Redis 的哈希底层结构和传统的哈希结构一样,由数组和链表组成。

哈希表中的每一个 key 都是独一无二的,程序可以根据 key 查找到与之关联的 value,或者通过 key 来更新 value,又或者根据 key 来删除整个 key-value等等。

哈希表优点在于,它能以 O(1) 的复杂度快速查询数据。将 key 通过 Hash 函数的计算,就能定位数据在表中的位置,因为哈希表实际上是数组,所以可以通过索引值快速查询到数据。

在哈希表大小固定的情况下,随着数据不断增多,那么哈希冲突的可能性也会越高。Redis 采用了「链式哈希」来解决哈希冲突,在不扩容哈希表的前提下,将具有相同哈希值的数据串起来,形成链接起,以便这些数据在表中仍然可以被查询到。

1.2 Hashes 类型内部编码及底层结构介绍

Redis 的哈希表结构如下:

1
2
3
4
5
6
7
typedef struct dictht {
    
    dictEntry **table;      // 哈希表数组
    unsigned long size;     // 哈希表大小
    unsigned long sizemask; // 哈希表大小掩码,用于计算索引值,总是等于 size - 1
    unsigned long used;     // 该哈希表已有的节点数量
} dictht;

哈希表由 dictht 结构定义:

  • table 属性是一个数组, 数组中的每个元素都是一个指向 dictEntry 结构的指针。
  • size 属性记录了哈希表的大小
  • used 属性则记录了哈希表目前已有节点(键值对)的数量。
  • sizemask 属性的值总是等于 size - 1 , 这个属性和哈希值一起决定一个键应该被放到 table 数组的哪个索引上面。

哈希表是一个 数组(dictEntry table) ,数组的每个元素是一个指向 哈希表节点(dictEntry) 的指针。 哈希表节点 dictEntry 的结构如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
typedef struct dictEntry {
    //键值对中的键
    void *key;
  
    //键值对中的值
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    //指向下一个哈希表节点,形成链表
    struct dictEntry *next;
} dictEntry;

哈希表节点使用 dictEntry 结构表示, 每个 dictEntry 结构都保存着一个键值对:

dictEntry 结构里不仅包含指向键和值的指针,还包含了指向下一个哈希表节点的指针,这个指针可以将多个哈希值相同的键值对链接起来,以此来解决哈希冲突的问题,这就是链式哈希。

dictEntry 结构里键值对中的值是一个 联合体 union v ,键值对中的值可以是一个指向实际值的指针,或者是一个无符号的 64 位整数或有符号的 64 位整数或double 类的值。这么做的好处是可以节省内存空间,因为当 是整数或浮点数时,就可以将值的数据内嵌在 dictEntry 结构里,无需再用一个指针指向实际的值,从而节省了内存空间。

字典结构定义:

1
2
3
4
5
6
7
typedef struct dict {
    dictType *type; // 类型特定函数
    void *privdata; // 私有数据
    dictht ht[2];   // 哈希表
    
    int rehashidx;  // rehash 索引, 当 rehash 不在进行时,值为 -1, rehashing not in progress if rehashidx == -1 
} dict;

字典由 dict 结构定义:

  • type 属性是一个指向 dictType 结构的指针,每个 dictType 结构保存了一簇用于操作特定类型键值对的函数,Redis 会为用途不同的字典设置不同的类型特定函数。
  • privdata 属性则保存了需要传给那些类型特定函数的可选参数。

type 属性和 privdata 属性是针对不同类型的键值对,为创建多态字典而设置的.

1
2
3
4
5
6
7
8
typedef struct dictType {
    unsigned int (*hashFunction)(const void *key);      // 计算哈希值的函数
    void *(*keyDup)(void *privdata, const void *key);   // 复制键的函数
    void *(*valDup)(void *privdata, const void *obj);   // 复制值的函数
    int (*keyCompare)(void *privdata, const void *key1, const void *key2);  // 对比键的函数
    void (*keyDestructor)(void *privdata, void *key);   // 销毁键的函数
    void (*valDestructor)(void *privdata, void *obj);   // 销毁值的函数
} dictType;
  • ht 属性是一个包含两个项的数组, 数组中的每个项都是一个 dictht 哈希表,

Tips: 一般情况下, 字典只使用 ht[0] 哈希表, ht[1] 哈希表只会在对 ht[0] 哈希表进行 rehash 时使用。

  • rehashidx 属性是另一个和 rehash 有关的属性,它记录了 rehash 目前的进度, 如果目前没有在进行 rehash , 那么它的值为 -1 。 普通状态下字典:

Redis 采用了 链式哈希 的方法来解决哈希冲突。实现的方式就是每个哈希表节点都有一个 next 指针,用于指向下一个哈希表节点,因此多个哈希表节点可以用 next 指针构成一个单项链表,被分配到同一个哈希桶上的多个节点可以用这个单项链表连接起来,这样就解决了哈希冲突。

在 Redis 中,哈希数据类型有两种内部编码方式,分别是 ziplist(压缩列表)hashtable(哈希表)。这两种编码方式的选择取决于哈希的大小和存储特性。

1、ziplist(压缩列表) 是 Redis 中用于内部编码较小哈希的紧凑数据结构。以下是一些关于 ziplist 的关键特性:

  • 当哈希类型的元素个数相对较少,且所有字段和对应的值都满足一定的限制条件时,Redis 会使用 ziplist 作为哈希的内部实现。
  • 默认情况下,Redis 会选择 ziplist。具体来说,如果哈希的元素(key-value对)个数不超过 512 个,并且所有值都小于 64 字节,那么 ziplist 就是首选的编码方式。
  • Ziplist 是一种紧凑的数据结构,它能够在节省内存方面表现得比 hashtable 更出色。它将多个哈希元素连续存储在一起,有效地减少了内存占用。

ziplist(压缩列表) 分布结构如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
area        |<---- ziplist header ---->|<----------- entries ------------->|<-end->|

size          4 bytes  4 bytes  2 bytes    ?        ?        ?        ?     1 byte
            +---------+--------+-------+--------+--------+--------+--------+-------+
component   | zlbytes | zltail | zllen | entry1 | entry2 |  ...   | entryN | zlend |
            +---------+--------+-------+--------+--------+--------+--------+-------+
                                       ^                          ^        ^
address                                |                          |        |
                                ZIPLIST_ENTRY_HEAD                |   ZIPLIST_ENTRY_END
                                                                  |
                                                         ZIPLIST_ENTRY_TAIL

2、hashtable(哈希表) 是 Redis 中用于存储大规模哈希数据的内部编码方式。以下是 hashtable 的关键特性:

  • 当哈希类型的元素个数超过了 ziplist 的配置限制,或者有字段对应的值大于 64 字节时,Redis 会将内部编码切换为 hashtable。
  • Hashtable 是一种散列表数据结构,它具有 O(1) 的读写时间复杂度,适用于大规模的哈希数据集。
  • 切换到 hashtable 可以提供更好的性能和内存管理,特别是在处理大型哈希或包含大值的情况下。

hashtable(哈希表) 针对指定的 key 进行散列计算后,可以映射到数组的一个位置,然后在指定的索引位置获取或存放数据,如果指定位置上存在数据(冲突),则进行链表遍历或添加链表,结构示图如下:

二、Hashes类型相关命令

2.1 Redis Hash 相关命令简介

以下是关于 Redis Hash 相关命令的简介表,包括命令、作用以及时间复杂度:

命令 作用 时间复杂度
HSET 在哈希表中设置字段的值,存在则更新,否则创建。可同时设置多组字段。 O(1)
HSETNX 仅在字段不存在时,在哈希表中设置字段的值,成功返回1,否则返回0。 O(1)
HMSET 同时将多个 field-value(域-值)对设置到哈希表 key 中。 O(N),N 为 field-value 对的数量。
HGET 获取指定哈希表中字段的值。 O(1)
HMGET 获取指定哈希表中多个字段的值。 O(N),N为字段数
HSCAN 命令用于迭代哈希键中的键值对。 O(N)
HKEYS 获取指定哈希表中所有字段的名称。 O(N),N为字段数
HVALS 获取指定哈希表中所有字段的值。 O(N),N为字段数
HGETALL 获取指定哈希表中所有字段和对应的值。 O(N),N为字段数
HEXISTS 检查指定哈希表中是否存在某个字段。 O(1)
HDEL 删除指定哈希表中的一个或多个字段。 O(N),N为被删除的字段数
HLEN 获取指定哈希表中字段的数量(即哈希表的大小)。 O(1)
HSTRLEN 返回哈希表 key 中,与指定域 field 相关联的值的字符串长度。 O(1)
HINCRBY 将哈希表中指定字段的值增加一个整数。 O(1)
HINCRBYFLOAT 将哈希表中指定字段的值增加一个浮点数。 O(1)

注意:时间复杂度中的 “N” 表示操作的复杂度与哈希表的长度或插入元素的数量成线性关系,而不是固定的常数时间。在实际使用中,需要根据数据规模和性能要求选择适当的命令。

2.2 HSET、HSETNX 和 HMSET 命令

1、HSET 命令 HSET 命令的作用是向指定的哈希表(key)中设置字段的值;如果 key 不存在,一个新的哈希表(key)被创建并进行 HSET 操作;如果字段存在则更新,否则创建;可以同时设置多组字段; 命令语法:

1
127.0.0.1:6379> HSET key field value [field value ... ]

命令返回值:

  • 如果 key 不存在,一个新的哈希表(key)被创建并进行 HSET 操作, 返回设置的 field 字段数;
  • 如果 field 列表中有 N个 是哈希表中的新建 field,并且值设置成功,返回 N;
  • 如果哈希表中域 field 全部 已经存在且旧值已被新值覆盖,返回 0;
  • 当 key 存在,但不是哈希表类型时,返回类型不匹配的错误信息;

2、HSETNX 命令 HSETNX 命令的作用是仅在字段不存在时,在指定的哈希表中设置字段的值。设置成功返回 1,否则返回 0;一次仅可设置一个field; 命令语法:

1
127.0.0.1:6379> HSETNX key field value 

HSETNX 命令返回值有 2 种情况:

  • 如果 key 不存在,一个新哈希表被创建并执行 HSETNX 命令。
  • 若设置成功,返回 1;
  • 如果指定field已经存在且没有操作被执行,返回 0
  • 当 key 存在,但不是哈希表类型时,返回类型不匹配的错误信息;

3、HMSET 命令 HMSET 命令同时将多个 field-value(域-值)对设置到哈希表 key 中,HMSET 会覆盖哈希表中已存在的域,如果 key 不存在,一个空哈希表被创建并执行 HMSET 操作。 命令语法:

1
127.0.0.1:6379> HMSET key field value [field value ...]

命令返回值:

  • 如果 key 不存在,一个新的哈希表(key)被创建并进行 HMSET 操作,返回 OK;
  • 如果哈希表 key 存在,field 存在的 value 会被覆盖,不存在的会被添加,返回 OK;
  • 当 key 存在,但不是哈希表类型时,返回类型不匹配的错误信息;

2.3 HGET、HMGET、HGETALL 和 HSCAN 命令

1、HGET 命令 HGET 命令的作用是获取指定哈希表(key)中指定字段(field)的值,一次只能获取一个field; 命令语法:

1
127.0.0.1:6379> HGET key field

HGET 命令返回有 4 种情况:

  • 当指定的哈希表 key 不存在时,返回 nil;
  • 当指定的哈希表 key 存在时,并且指定的field 也存在,返回field 的值;
  • 当指定的哈希表 key 存在时,并且指定的field 不存在,返回 nil;
  • 当指定的 key 不是哈希表时,返回类型不匹配的错误信息;

2、HMGET 命令 HMGET 命令返回哈希表 key 中,一个或多个指定 field 的值,指定多个 field 的关联值的列表排列顺序和指定 field 参数的请求顺序一样。 命令语法:

1
127.0.0.1:6379> HMGET key field [field ...]

HMGET 命令返回有 3 种情况:

  • 当指定的哈希表 key 不存在时,返回 与 field 数量相同的 nil(列表);
  • 当指定的哈希表 key 存在时,返回 与 field 位置对应的 value 列表,如果指定的 field 不存在,对应位置为 nil ;
  • 当指定的 key 不是哈希表时,返回类型不匹配的错误信息;

3、HGETALL 命令 HGETALL 命令的作用是获取哈希表 key 中,所有的 field 和 value值,在返回值里,每 field 之后是其对应的 value 值,所以返回值的长度是哈希表大小的两倍。 命令语法:

1
127.0.0.1:6379> HGETALL key field [field ...]

HGETALL 命令返回有 3 种情况:

  • 当指定的哈希表 key 不存在时,返回 (empty array);
  • 当指定的哈希表 key 存在时,以列表形式返回哈希表所有的 field 和 field对应的 value 值,在返回值里,每 field 之后是其对应的 value 值,然后是下一个field 及 下一个field对应的值,依次类推,直至返回hash 表 的全部field-value 对;
  • 当指定的 key 不是哈希表时,返回类型不匹配的错误信息;

4、HSCAN 命令 HSCAN 命令用于迭代哈希键中的键值对。 命令语法:

1
127.0.0.1:6379> HSCAN key cursor [MATCH pattern] [COUNT count]

cursor 游标位置,游标参数被设置为0时,服务器将开始一次新的迭代。

2.4 HKEYS 和 HVALS 命令

1、HKEYS 命令 HKEYS 命令的作用是获取指定哈希表中所有字段的名称。 命令语法:

1
127.0.0.1:6379> HKEYS key

HKEYS 命令返回有 3 种情况:

  • 当指定的哈希表 key 不存在时,返回 (empty array);
  • 当指定的哈希表 key 存在时,返回一个包含哈希表中所有 fields 的列表;
  • 当指定的 key 不是哈希表时,返回类型不匹配的错误信息;

2、HVALS 命令 HVALS 命令的作用是获取指定哈希表中所有字段的value值。 命令语法:

1
127.0.0.1:6379> HVALS key

HVALS 命令返回有 3 种情况:

  • 当指定的哈希表 key 不存在时,返回 (empty array);
  • 当指定的哈希表 key 存在时,命令返回一个包含哈希表中所有 fields 的值 的列表;
  • 当指定的 key 不是哈希表时,返回类型不匹配的错误信息;

2.5 HEXISTS、HLEN、HSTRLEN 和 HDEL 命令

HEXISTS 命令 HEXISTS 命令的作用是查看哈希表 key 中,指定的 field 是否存在。 命令语法:

1
127.0.0.1:6379> HEXISTS key field

HEXISTS 命令返回有 3 种情况:

  • 当指定的哈希表 key 不存在 或 哈希表中指定的 field 不存在时,返回 0;
  • 当哈希表中指定的 field 存在,返回 1;
  • 当指定的 key 不是哈希表时,返回类型不匹配的错误信息;

2、HLEN 命令 HLEN 命令返回哈希表 key 中 field 的数量,当散列有可能包含的值非常大,那么用户可以先使用 HLEN 命令获取出散列包含的域的数量,然后再结合 HKEYS 命令列出所有域名,再进一步可以使用 HGET 一个接一个地取出域的值,从而避免因为一次获取多个大体积的值而导致服务器阻塞。 命令语法:

1
127.0.0.1:6379> HLEN key

HLEN 命令返回 3 种情况:

  • 当指定的 key 存在时,返回哈希表中域的数量;
  • 当指定的 key 不存在时,返回 0;
  • 当指定的 key 不是哈希类型时,返回类型不匹配的错误信息;

3、HSTRLEN 命令 HSTRLEN 命令返回哈希表 key 中,与指定域 field 相关联的值的字符串长度。 命令语法:

1
127.0.0.1:6379> HSTRLEN key field

HSTRLEN 命令返回值有 2 种情况:

  • 如果指定的 key 或者 filed 不存在时,那么命令返回 0;
  • 如果指定的 key 且 filed 存在时,返回字符串的长度;
  • 当指定的 key 不是哈希类型时,返回类型不匹配的错误信息;

4、HDEL 命令 HDEL 命令删除哈希表 key 中的一个或多个指定域,不存在的域将被忽略。2.4 版本之前,HDEL 命令每次只能删除单个指定域;在 2.4 版本之前,如果需要在一个原子时间内删除多个域,需将命令包含在 MULTI/EXEC 块内;在 2.4 版本及之后,HDEL 命令才支持一次删除多个域操作。 命令语法:

1
127.0.0.1:6379> HDEL key field [field ...]

命令参数

  • key:哈希表指定的键。
  • filed:哈希表中指定的域。

HDEL 命令返回有 3 种情况:

  • 当指定的哈希表 key 不存在 或 哈希表key 存在 但 指定的 fields 不存在时,返回 0;
  • 当哈希表中指定的 field 部分或全部存在,返回被成功移除的 field(存在的field)的数量,不存在的 field 被忽略;
  • 当指定的 key 不是哈希表时,返回类型不匹配的错误信息;

2.6 HINCRBY 和 HINCRBYFLOAT 命令

1、HINCRBY 命令 HINCRBY 命令为哈希表 key 中的域 field 的值加上增量 increment。增量也可以为负数,相当于对指定域进行减法操作。 命令语法:

1
127.0.0.1:6379> HINCRBY key field increment

如果 key 不存在,一个新的哈希表被创建并执行 HINCRBY 命令。如果 field 不存在,那么在执行命令前,域的值被初始化为 0。

对一个储存字符串值的域 field 执行 HINCRBY 命令将造成一个错误。

HINCRBY 操作的值被限制在 64 位有符号数字表示之内。

命令返回值:

  • 如果 key 不存在则创建 哈希表 key 并执行 HINCRBY 命令,field的值被初始化为 0,然后再加上 increment,返回修改后的值 increment;
  • 如果 哈希表 key 存在,field 不存在,那么在执行 HINCRBY 命令前,field的值被初始化为 0,然后再加上 increment,返回修改后的值 increment;
  • 如果 哈希表 key 存在,field 对应的 value 为整数,则value值 加上 increment 后,返回修改后的值 value + increment;
  • 如果 哈希表 key 存在,field 对应的 value 不为整数(为字符串、或浮点数deng),返回错误提示信息 (error) ERR hash value is not an integer
  • 当指定的 key 不是哈希表时,返回类型不匹配的错误信息;

2、HINCRBYFLOAT 命令 HINCRBYFLOAT 命令为哈希表 key 中的域 field 的值加上浮点数增量 increment。增量也可以为负浮点数,相当于对指定域进行减法操作。

HINCRBYFLOAT 命令的详细功能和 INCRBYFLOAT 命令类似。

命令语法:

1
127.0.0.1:6379> HINCRBYFLOAT key field increment

如果键 key 不存在,那么 HINCRBYFLOAT 会先创建一个哈希表 key,再创建域 field 并将域 field 的值设为 0,最后再执行加法操作;如果哈希表 key 中没有域 field,那么 HINCRBYFLOAT 会先将域 field 的值设为 0,然后再执行加法操作。

命令返回值:

  • 如果 key 不存在则创建 哈希表 key 并执行 HINCRBYFLOAT 命令,field的值被初始化为 0,然后再加上 increment,返回修改后的值 increment;
  • 如果 哈希表 key 存在,field 不存在,那么在执行 HINCRBY 命令前,field的值被初始化为 0,然后再加上 increment,返回修改后的值 increment;
  • 如果 哈希表 key 存在,field 对应的 value 为整数或浮点数,则value值 加上 increment 后,返回修改后的值 value + increment;
  • 如果 哈希表 key 存在,field 对应的 value 不为整数或浮点数(为字符串等),返回错误提示信息 (error) ERR hash value is not an integer
  • 当指定的 key 不是哈希表时,返回类型不匹配的错误信息;

三、Hashes 类型的应用场景

3.1 哈希类型用于对象数据存储场景

举例:用户信息、商品信息、文章信息、购物车信息。