<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> 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> hget user:1 name?
"tom"
如果鍵或field不存在,會傳回nil:
127.0.0.1:6379> hget user:2 name?
(nil)?
127.0.0.1:6379> hget user:1 age?
(nil)
(3)删除field
hdel key field [field ...]
hdel會删除一個或多個field,傳回結果為成功删除field的個數,例如:
127.0.0.1:6379> hdel user:1 name?
127.0.0.1:6379> hdel user:1 age?
(integer) 0
(4)計算field個數
hlen key
例如user:1有3個field:
(integer) 1?
127.0.0.1:6379> hset user:1 age 23
127.0.0.1:6379> hset user:1 city tianjin
127.0.0.1:6379> 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> hmset user:1 name mike
age 12 city tianjin
ok?
127.0.0.1:6379> hmget user:1 name city
1) "mike"
2) "tianjin"
(6)判斷field是否存在
hexists key field
例如,user:1包含name域,是以傳回結果為1,不包含時傳回0:
127.0.0.1:6379> hexists user:1 name
(7)擷取所有field
hkeys key
hkeys指令應該叫hfields更為恰當,它傳回指定哈希鍵所有的field,例如:
127.0.0.1:6379> hkeys user:1
1) "name"?
2) "age"?
3) "city"
(8)擷取所有value
hvals key
下面操作擷取user:1全部value:
127.0.0.1:6379> hvals user:1?
1) "mike"?
2) "12"?
3) "tianjin"
(9)擷取所有的field-value
hgetall key
下面操作擷取user:1所有的field-value:
127.0.0.1:6379> 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> 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> hmset hashkey f1 v1 f2
v2
ok
127.0.0.1:6379> object encoding hashkey
"ziplist"
2.1)當有value大于64位元組,内部編碼會由ziplist變為hashtable:
127.0.0.1:6379> 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會消耗更多記憶體。