Redis的數據類型之 hash

書接上回

前一篇文章,我們學習的是 Redis的數據結構 list, 學習了其基本的操作和使用內部數據結構是quicklistziplist,這兩種數據結構雖然起得名字是list,但是其內部結構確實鏈表。如果不記得了其內部構成, 就再看看看着上篇文章吧。現在我們繼續學習下一個數據類型 hash

hash簡介

hash 是一個鍵值對集合. 是 string 類型的 keyvalue 的映射表, hash 特別適合用於存儲對象, 每個hash 類型可以存儲 2^32-1 個鍵值對。

hash 實際上就是一個 哈希表。類似於 Java 裏的HashTable

但是 Redis 的哈希是有兩種數據結構(內部編碼)來表示的。

  • 一種是 ziplist ,上篇文章中我們簡單的介紹了ziplist的內部構成,見 Redis的數據結構 list, 以及ziplist的編碼方式, 可以看這篇文章 10-Redis的數據結構之ziplist.md. Redis 什麼時候會使用ziplist 這種編碼方式呢?

    • hash類型的元素的個數小於 hash-max-ziplist-enties配置,默認512.
    • 所有的值都小於hash-max-ziplist-value的值,默認是64個字節的時候。
      當同時滿足以上兩個條件的時候, 就會使用 ziplist 這種結構。

這種方式最大的優點就是節約空間。

  • 另一種就是使用 hashtable 來編碼了。當不滿足上面提及的兩個條件時,就會使用 hashtable 來編碼。實際上是 dict 這種數據結構。這裏我們又可以學習到一個新的數據結構 dict

hash的應用場景

  • 緩存對象信息: 對象的每個屬性對應着hash的一個鍵值對。改變的時候,只需要改變對應的某個filed-value即可。
  • 緩存購物車的信息: 用戶的idkey, 商品的idfield. 商品的數量爲value。 比如: hset userId productId productCount

hash的基本命令

hset

  • 語法

hset key field value

  • 解釋

將哈希表 hash 中域 field 的值設置爲 value

如果給定的哈希表並不存在, 那麼一個新的哈希表將被創建並執行 HSET 操作。

如果域 field 已經存在於哈希表中, 那麼它的舊值將被新值 value 覆蓋。

  • 演示
## 設置一個hash結構
127.0.0.1:6379> HSET k38 f1 v38
(integer) 1
# 獲取一個字段
127.0.0.1:6379> HGET k38 f1
"v38"
# 設置一個已經存在的值, 注意返回的值。
127.0.0.1:6379> HSET k38 f1 v38v38
(integer) 0
127.0.0.1:6379> HGET k38 f1
"v38v38"

hsetnx

  • 語法

HSETNX key field value

  • 解釋

當且僅當域 field 尚未存在於哈希表的情況下, 將它的值設置爲 value

如果給定域已經存在於哈希表當中, 那麼命令將放棄執行設置操作。

如果哈希表 hash 不存在, 那麼一個新的哈希表將被創建並執行 HSETNX 命令。

  • 演示
# 設置一個不存在的 key
127.0.0.1:6379> HSETNX k39 f1 v39
(integer) 1
127.0.0.1:6379> HGET k39 f1
"v39"
# 再次設置
127.0.0.1:6379> HSETNX k39 f1 v39v39
(integer) 0
127.0.0.1:6379> HGET k39 f1
"v39"

hget

這個命令上面已經用到了。這裏就不浪費時間了。

  • 語法

HGET key field

  • 解釋

獲取對應的 key 下的域 field 的值。不存在的時候,返回 nil

hgetall

  • 語法

HGETALL key

  • 解釋

返回哈希表 key 中,所有的域和值。

在返回值裏,緊跟每個域名(field name)之後是域的值(value),所以返回值的長度是哈希表大小的兩倍。

  • 演示
127.0.0.1:6379> HGETALL k39 
1) "f1"
2) "v39"
127.0.0.1:6379> hset k39 f2 v39_2
(integer) 1
127.0.0.1:6379> HGETALL k39 
1) "f1"
2) "v39"
3) "f2"
4) "v39_2"

hexists

  • 語法

HEXISTS key field

  • 解釋

檢查給定域 field 是否存在於哈希表 hash 當中。

存在返回1,不存在返回0

  • 演示
127.0.0.1:6379> HEXISTS k40 f1
(integer) 0
127.0.0.1:6379> HSET k40 f1 v40
(integer) 1
127.0.0.1:6379> HEXISTS k40 f1
(integer) 1

del

  • 語法

HDEL key field [field ...]

  • 解釋

刪除哈希表 key 中的一個或多個指定域,不存在的域將被忽略。

  • 演示
127.0.0.1:6379> HSET k41 f1 v41_1
(integer) 1
127.0.0.1:6379> HSET k41 f2 v41_2
(integer) 1
127.0.0.1:6379> HSET k41 f3 v41_3
(integer) 1
127.0.0.1:6379> HGETALL k41
1) "f1"
2) "v41_1"
3) "f2"
4) "v41_2"
5) "f3"
6) "v41_3"
127.0.0.1:6379> HDEL k41 f1 f3 f4 
(integer) 2
127.0.0.1:6379> HGETALL k41
1) "f2"
2) "v41_2"

hlen

  • 語法

HLEN key

  • 解釋

返回哈希表 key 中域的數量。

  • 演示
127.0.0.1:6379> HSET k42 f1 v42_1
(integer) 1
127.0.0.1:6379> HSET k42 f2 v42_2
(integer) 1
127.0.0.1:6379> HSET k42 f3 v42_3
(integer) 1
127.0.0.1:6379> hlen k42
(integer) 3

hstrlen

  • 語法

HSTRLEN key field

  • 解釋

返回哈希表 key 中, 與給定域 field 相關聯的值的字符串長度(string length)。

如果給定的鍵或者域不存在, 那麼命令返回 0

  • 演示
127.0.0.1:6379> HSET k43 f1 "Hello World"
(integer) 1
127.0.0.1:6379> HSTRLEN k43 f1
(integer) 11
127.0.0.1:6379> HSTRLEN k43 f2
(integer) 0

  • 語法

HINCRBY key field increment

  • 解釋

爲哈希表 key 中的域 field 的值加上增量 increment

增量也可以爲負數,相當於對給定域進行減法操作。

如果 key 不存在,一個新的哈希表被創建並執行 HINCRBY 命令。

如果域 field 不存在,那麼在執行命令前,域的值被初始化爲 0

對一個儲存字符串值的域 field 執行 HINCRBY 命令將造成一個錯誤。

本操作的值被限制在 64 位(bit)有符號數字表示之內。

  • 演示
# 不存在的key與域 field
127.0.0.1:6379> HINCRBY k45 f1 100
(integer) 100
127.0.0.1:6379> HINCRBY k45 f1 -200
(integer) -100
127.0.0.1:6379> HINCRBY k45 f1 200
(integer) 100
# 錯誤的類型
127.0.0.1:6379> HSET k45 f2 v45
(integer) 1
127.0.0.1:6379> HINCRBY k45 f2 100
(error) ERR hash value is not an integer

hincrbyfloat

  • 語法

HINCRBYFLOAT key field increment

  • 解釋

爲哈希表 key 中的域 field 加上浮點數增量 increment

如果哈希表中沒有域 field ,那麼 HINCRBYFLOAT 會先將域 field 的值設爲 0 ,然後再執行加法操作。

如果鍵 key 不存在,那麼 HINCRBYFLOAT 會先創建一個哈希表,再創建域 field ,最後再執行加法操作。

  • 演示
127.0.0.1:6379> HINCRBYFLOAT  k46 f1 100.5
"100.5"
127.0.0.1:6379> HINCRBYFLOAT  k46 f1 100.5
"201"
127.0.0.1:6379> HINCRBYFLOAT  k46 f1 -100.5
"100.5"
127.0.0.1:6379> HSET k46 f2 v46_2
(integer) 1

hmset

  • 語法

HMSET key field value [field value ...]

  • 解釋

同時將多個 field-value (域-值)對設置到哈希表 key 中。

此命令會覆蓋哈希表中已存在的域。

如果 key 不存在,一個空哈希表被創建並執行 HMSET 操作。

  • 演示
127.0.0.1:6379> HMSET k47  f1 v47_1 f2 v47_2 f3 v47_3
OK
127.0.0.1:6379> HGETALL k47
1) "f1"
2) "v47_1"
3) "f2"
4) "v47_2"
5) "f3"
6) "v47_3"

hmget

  • 語法

HMGET key field [field ...]

  • 解釋

返回哈希表 key 中,一個或多個給定域的值。

如果給定的域不存在於哈希表,那麼返回一個 nil 值。

因爲不存在的 key 被當作一個空哈希表來處理,所以對一個不存在的 key 進行 HMGET 操作將返回一個只帶有 nil 值的表。

  • 演示
127.0.0.1:6379> HMSET k48 f1 v1 f2 v2 f3 v3 f4 v4
OK
127.0.0.1:6379> hmget k48 f1 f3 f4
1) "v1"
2) "v3"
3) "v4"
127.0.0.1:6379> 

hkeys

  • 語法

HKEYS key

  • 解釋

返回哈希表 key 中的所有域。

key 不存在時,返回一個空表。

  • 演示
127.0.0.1:6379> HMSET k49 f1 v1 f2 v2 f3 v3 f4 v4
OK
127.0.0.1:6379> HKEYS k49
1) "f1"
2) "f2"
3) "f3"
4) "f4"

hvals

  • 語法

HVALS key

  • 解釋

返回 key 對應的所有的value

  • 演示
127.0.0.1:6379> HMSET k50 f1 v1 f2 v2 f3 v3 f4 v4 
OK
127.0.0.1:6379> HVALS k50
1) "v1"
2) "v2"
3) "v3"
4) "v4"

hscan

  • 語法

HSCAN key cursor [MATCH pattern] [COUNT count]

  • 解釋

這是一個查詢命令。 同 SCAN 命令. 可以參考這篇文章 010-其他命令

SCAN 命令是一個基於遊標的迭代器(cursor based iterator): SCAN 命令每次被調用之後, 都會向用戶返回一個新的遊標, 用戶在下次迭代時需要使用這個新遊標作爲 SCAN 命令的遊標參數, 以此來延續之前的迭代過程。

  • 演示
127.0.0.1:6379> HMSET k51  f1 v1 f2 v2 f3 v3 f4 v4 f5 v5 f6 v6 f7 v7 f8 v8
OK
127.0.0.1:6379> hscan k51 0 
1) "0"
2)  1) "f1"
    2) "v1"
    3) "f2"
    4) "v2"
    5) "f3"
    6) "v3"
    7) "f4"
    8) "v4"
    9) "f5"
   10) "v5"
   11) "f6"
   12) "v6"
   13) "f7"
   14) "v7"
   15) "f8"
   16) "v8"

以上,就是 Redishash類型相關的15個命令了。務必熟記~

hash的內部結構

hash類型簡介的時候,我們就說過 hash是用兩種數據結構來編碼的。

  • ziplist

  • hashtable(dict)

ziplist 之前已經分享過了。具體參考之前的文章吧。 [鏈接]

這裏我們就簡單的來看下 hashtable.

我們直接搜索 hash ,可以發現 t_hash.c 這個文件,引入了 server.h . 大體看了一下,都是函數的實現。那我們看下 server.h ,應該存在對 hastable的定義吧。然而,並沒有。

那我們來看下t_hash.c中添加方法的實現吧. int hashTypeSet(robj *o, sds field, sds value, int flags)

源碼太長了,這裏就不粘了, 可以看源碼

通過查看源碼可以得出:

  • hash類型的默認編碼是 OBJ_ZIPLIST. 即默認是使用 ziplist 這種數據結構進行編碼存儲的。
robj *createHashObject(void) {
    unsigned char *zl = ziplistNew();
    robj *o = createObject(OBJ_HASH, zl);
    o->encoding = OBJ_ENCODING_ZIPLIST;
    return o;
}
  • hash元素的個數大於 hash_max_ziplist_entries 時會,轉換成 hashTable(OBJ_ENCODING_HT),
...
 if (hashTypeLength(o) > server.hash_max_ziplist_entries)
            hashTypeConvert(o, OBJ_ENCODING_HT);
...

但是在 redis 5.0.7 中暫時不支持這種方式, 還沒有實現。(沒有實現從ziplist編碼轉化成hash編碼。)

void hashTypeConvert(robj *o, int enc) {
    if (o->encoding == OBJ_ENCODING_ZIPLIST) {
        hashTypeConvertZiplist(o, enc);
    } 
    /// 這裏!!!
    else if (o->encoding == OBJ_ENCODING_HT) {
        serverPanic("Not implemented");
    } else {
        serverPanic("Unknown hash encoding");
    }
}
  • 當創建的hash類型是 hashtable 編碼(OBJ_ENCODING_HT)時,是使用dict這種類型存儲的.
/// dict類型
typedef struct dict {
    dictType *type;
    void *privdata;
    /// 2個哈希表來實現
    dictht ht[2];
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */
    unsigned long iterators; /* number of iterators currently running */
} dict;

/// 哈希表實現
typedef struct dictht {
    dictEntry **table; /// 哈希表節點指針數據(java源碼中的桶的概念)
    unsigned long size; /// 指針數組的大小
    unsigned long sizemask; /// 指針數據的長度掩碼,用於計算索引值
    unsigned long used; /// 哈希表現有的節點數量
} dictht;

///哈希表的節點
typedef struct dictEntry {
    /// 鍵
    void *key;
    /// 值
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    /// 下一個節點: dictht 是使用鏈地址法來處理hash衝突。
    struct dictEntry *next;
} dictEntry;

整個 dict 結構就可以這麼表示:

在這裏插入圖片描述

到這裏,我們就知道了 hash 這種類型,是如何存儲的了。 如果你還想了解
dict 是如何 rehash, 擴容,縮容。以及 dict api相關實現的話,移駕這篇文章吧。 起駕 ~

總結

  • hash結構,是一種哈希表結構。通過兩種數據結構ziplisthashtable(dict)實現。
  • 要熟練掌握的 hash 相關的15個命令。
  • hashtable的編碼格式, 實際上就是使用的 dict這種編碼方式。我們簡單的學習了Redisdict結構的實現。還有一篇專門的文章,來介紹 dict的詳細內容。

最後

希望和你成爲朋友!我們一起學習~
最新文章盡在公衆號【方家小白】,期待和你相逢在【方家小白】

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章