天天看點

Redis開發與運維. 2.3 哈希

<b>2.3 哈希</b>

幾乎所有的程式設計語言都提供了哈希(hash)類型,它們的叫法可能是哈希、字典、關聯數組。在redis中,哈希類型是指鍵值本身又是一個鍵值對結構,形如value={{field1,value1},...{fieldn,valuen}},redis鍵值對和哈希類型二者的關系可以用圖2-14來表示。

圖2-14 字元串和哈希類型對比

哈希類型中的映射關系叫作field-value,注意這裡的value是指field對應的值,不是鍵對應的值,請注意value在不同上下文的作用。

<b>2.3.1 指令</b>

(1)設定值

hset key field value

下面為user:1添加一對field-value:

127.0.0.1:6379&gt; hset user:1 name tom?

(integer) 1

如果設定成功會傳回1,反之會傳回0。此外redis提供了hsetnx指令,它們的關系就像set和setnx指令一樣,隻不過作用域由鍵變為field。

(2)擷取值

hget key field

例如,下面操作擷取user:1的name域(屬性)對應的值:

127.0.0.1:6379&gt; hget user:1 name?

"tom"

如果鍵或field不存在,會傳回nil:

127.0.0.1:6379&gt; hget user:2 name?

(nil)?

127.0.0.1:6379&gt; hget user:1 age?

(nil)

(3)删除field

hdel key field [field ...]

hdel會删除一個或多個field,傳回結果為成功删除field的個數,例如:

127.0.0.1:6379&gt; hdel user:1 name?

127.0.0.1:6379&gt; hdel user:1 age?

(integer) 0

(4)計算field個數

hlen key

例如user:1有3個field:

(integer) 1?

127.0.0.1:6379&gt; hset user:1 age 23

127.0.0.1:6379&gt; hset user:1 city tianjin

127.0.0.1:6379&gt; hlen user:1

(integer) 3

(5)批量設定或擷取field-value

hmget key field [field ...]?

hmset key field value [field value ...]

hmset和hmget分别是批量設定和擷取field-value,hmset需要的參數是key和多對field-value,hmget需要的參數是key和多個field。例如:

127.0.0.1:6379&gt; hmset user:1 name mike

age 12 city tianjin

ok?

127.0.0.1:6379&gt; hmget user:1 name city

1) "mike"

2) "tianjin"

(6)判斷field是否存在

hexists key field

例如,user:1包含name域,是以傳回結果為1,不包含時傳回0:

127.0.0.1:6379&gt; hexists user:1 name

(7)擷取所有field

hkeys key

hkeys指令應該叫hfields更為恰當,它傳回指定哈希鍵所有的field,例如:

127.0.0.1:6379&gt; hkeys user:1

1) "name"?

2) "age"?

3) "city"

(8)擷取所有value

hvals key

下面操作擷取user:1全部value:

127.0.0.1:6379&gt; hvals user:1?

1) "mike"?

2) "12"?

3) "tianjin"

(9)擷取所有的field-value

hgetall key

下面操作擷取user:1所有的field-value:

127.0.0.1:6379&gt; hgetall user:1

1) "name"

2) "mike"

3) "age"

4) "12"

5) "city"

6) "tianjin"

在使用hgetall時,如果哈希元素個數比較多,會存在阻塞redis的可能。如果開發人員隻需要擷取部分field,可以使用hmget,如果一定要擷取全部field-value,可以使用hscan指令,該指令會漸進式周遊哈希類型,hscan将在2.7節介紹。

(10)hincrby hincrbyfloat

hincrby key field

hincrbyfloat key field

hincrby和hincrbyfloat,就像incrby和incrbyfloat指令一樣,但是它們的作用域是filed。

(11)計算value的字元串長度(需要redis 3.2以上)

hstrlen key field

例如hget user:1 name的value是tom,那麼hstrlen的傳回結果是3:

127.0.0.1:6379&gt; hstrlen user:1 name

表2-3是哈希類型指令的時間複雜度,開發人員可以參考此表選擇适合的指令。

表2-3 哈希類型指令的時間複雜度

命  令         時間複雜度

hset key field value  o(1)

hget key field   o(1)

hdel key field [field ...]      o(k),k是field個數

hlen key   o(1)

hgetall key        o(n),n是field總數

hmget field [field ...]         o(k),k是field的個數

hmset field value [field value ...]      o(k),k是field的個數

hexists key field        o(1)

hkeys key o(n),n是field總數

hvals key  o(n),n是field總數

hsetnx key field value       o(1)

hincrby key field increment      o(1)

hincrbyfloat key field increment      o(1)

hstrlen key field        o(1)?

<b>2.3.2 内部編碼</b>

哈希類型的内部編碼有兩種:

ziplist(壓縮清單):當哈希類型元素個數小于hash-max-ziplist-entries配置(預設512個)、同時所有值都小于hash-max-ziplist-value配置(預設64位元組)時,redis會使用ziplist作為哈希的内部實作,ziplist使用更加緊湊的結構實作多個元素的連續存儲,是以在節省記憶體方面比hashtable更加優秀。

hashtable(哈希表):當哈希類型無法滿足ziplist的條件時,redis會使用hashtable作為哈希的内部實作,因為此時ziplist的讀寫效率會下降,而hashtable的讀寫時間複雜度為o(1)。

下面的示例示範了哈希類型的内部編碼,以及相應的變化。

1)當field個數比較少且沒有大的value時,内部編碼為ziplist:

127.0.0.1:6379&gt; hmset hashkey f1 v1 f2

v2

ok

127.0.0.1:6379&gt; object encoding hashkey

"ziplist"

2.1)當有value大于64位元組,内部編碼會由ziplist變為hashtable:

127.0.0.1:6379&gt; hset hashkey f3

"one string is bigger than 64 byte...忽略..."

"hashtable"

2.2)當field個數超過512,内部編碼也會由ziplist變為hashtable:

v2 f3 v3 ...忽略... f513 v513

有關哈希類型的記憶體優化技巧将在8.3節中詳細介紹。

<b>2.3.3 使用場景</b>

圖2-15為關系型資料表記錄的兩條使用者資訊,使用者的屬性作為表的列,每條使用者資訊作為行。

如果将其用哈希類型存儲,如圖2-16所示。

相比于使用字元串序列化緩存使用者資訊,哈希類型變得更加直覺,并且在更新操作上會更加便捷。可以将每個使用者的id定義為鍵字尾,多對field-value對應每個使用者的屬性,類似如下僞代碼:

userinfo getuserinfo(long id){?

// 使用者id作為key字尾

userrediskey = "user:info:" + id;

// 使用hgetall擷取所有使用者資訊映射關系

userinfomap = redis.hgetall(userrediskey);

userinfo userinfo;?

if (userinfomap != null) {?

// 将映射關系轉換為userinfo

userinfo = transfermaptouserinfo(userinfomap);?

    }

else {

// 從mysql中擷取使用者資訊

userinfo = mysql.get(id);

// 将userinfo變為映射關系使用hmset儲存到redis中

redis.hmset(userrediskey, transferuserinfotomap(userinfo));

// 添加過期時間?

redis.expire(userrediskey, 3600);

}?

return userinfo;?

}

但是需要注意的是哈希類型和關系型資料庫有兩點不同之處:

哈希類型是稀疏的,而關系型資料庫是完全結構化的,例如哈希類型每個鍵可以有不同的field,而關系型資料庫一旦添加新的列,所有行都要為其設定值(即使為null),如圖2-17所示。

關系型資料庫可以做複雜的關系查詢,而redis去模拟關系型複雜查詢開發困難,維護成本高。

圖2-17 關系型資料庫稀疏性

開發人員需要将兩者的特點搞清楚,才能在适合的場景使用适合的技術。到目前為止,我們已經能夠用三種方法緩存使用者資訊,下面給出三種方案的實作方法和優缺點

分析。

1)原生字元串類型:每個屬性一個鍵。

set user:1:name tom

set user:1:age 23?

set user:1:city beijing

優點:簡單直覺,每個屬性都支援更新操作。

缺點:占用過多的鍵,記憶體占用量較大,同時使用者資訊内聚性比較差,是以此種方案一般不會在生産環境使用。

2)序列化字元串類型:将使用者資訊序列化後用一個鍵儲存。

set user:1 serialize(userinfo)

優點:簡化程式設計,如果合理的使用序列化可以提高記憶體的使用效率。

缺點:序列化和反序列化有一定的開銷,同時每次更新屬性都需要把全部資料取出進行反序列化,更新後再序列化到redis中。

3)哈希類型:每個使用者屬性使用一對field-value,但是隻用一個鍵儲存。

hmset user:1 name tom?age 23 city beijing

優點:簡單直覺,如果使用合理可以減少記憶體空間的使用。

缺點:要控制哈希在ziplist和hashtable兩種内部編碼的轉換,hashtable會消耗更多記憶體。