書接上回
前一篇文章,我們學習的是 Redis的數據結構 list, 學習了其基本的操作和使用內部數據結構是quicklist
和ziplist
,這兩種數據結構雖然起得名字是list
,但是其內部結構確實鏈表。如果不記得了其內部構成, 就再看看看着上篇文章吧。現在我們繼續學習下一個數據類型 hash
hash
簡介
hash
是一個鍵值對集合. 是 string
類型的 key
和 value
的映射表, 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
即可。 - 緩存購物車的信息: 用戶的
id
爲key
, 商品的id
爲field
. 商品的數量爲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"
以上,就是 Redis
中hash
類型相關的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
結構,是一種哈希表結構。通過兩種數據結構ziplist
和hashtable
(dict
)實現。- 要熟練掌握的
hash
相關的15
個命令。 hashtable
的編碼格式, 實際上就是使用的dict
這種編碼方式。我們簡單的學習了Redis
中dict
結構的實現。還有一篇專門的文章,來介紹dict
的詳細內容。
最後
希望和你成爲朋友!我們一起學習~
最新文章盡在公衆號【方家小白】,期待和你相逢在【方家小白】