一、哈希對象簡介
幾乎所有的編程語言都提供了哈希(hash)類型,它們的叫法可能是哈希、字典、關聯數組
哈希又稱散列
在Redis中,哈希類型是指鍵值本身又是一個鍵值對結構,形如value={{field1,value1},...{fieldN,valueN}},Redis鍵值對和哈希類型二者的關系可以下圖表示
一些特點:
存儲多個鍵值對之間的映射,并且鍵值對不允許重復
在某一個固定的key中,其對應value中的field也不允許重復
散列存儲的值既可以是字符串也可以是數字值
用戶同樣可以對散列存儲的數字值執行自增操作或自減操作
散列在很多方面是一個微縮版的Redis,不少字符串命令都有相應的散列版本
熟悉文檔數據庫的讀者可以將散列看作是文檔數據庫里面的文檔,而熟悉關系數據庫的讀者可以將散列看作是關系數據庫里面的行。因為“文檔、行、散列”這三者都允許用戶同時訪問或修改一個或多個域
注意:哈希類型中的映射關系叫作field-value,注意這里的value是指field對應的值,不是鍵對應的值,請注意value在不同上下文的作用。
二、命令
常用命令
hset:設置值。如果設置成功會返回1,反之會返回0
hset key field value
? ? ??hsetnx:它們的關系就像set和setnx命令一樣,只不過作用域由鍵變為field
? ? ? ?
hget:獲取值。如果鍵或field不存在,返回nil
hget key field
好,到這里會有人好奇,這里到底是怎么樣的結構,能不能直觀的看到這些記錄,還是我在前幾篇文章說到的Redis DeskTop Manager可以查看
hdel:刪除field
hdel會刪除一個或多個field,返回結果為成功刪除field的個數
直到某一個key對應的field全部刪除完全之后,該哈希對象才會被刪除
hdel key field [field ...]
hlen:計算fileld個數
hlen key
hmget、hmset:批量獲取/設置field-value
hmget key field [field ...]
hmset key field value [field value ...]
hstrlen:計算value的字符串長度(需要Redis3.2以上)
hstrlen key field
其他命令
hincrby、hincrbyfloat:hincrby和hincrbyfloat,就像incrby和incrbyfloat命令一樣,但是它們的作用域是filed
hexists:判斷field是否存在。field存在返回1,不包含返回0
hkeys:獲取所有field
hkeys key
hvals:獲取所有值
hvals key
hgetall:獲取所有的field-value
hgetall key
提示:在使用hgetall時,如果哈希元素個數比較多,會存在阻塞Redis的可能。 如果開發人員只需要獲取部分field,可以使用hmget,如果一定要獲取全部 field-value,可以使用hscan命令,該命令會漸進式遍歷哈希類型.
下圖給出了哈希類型命令的時間復雜度:
三、內部編碼
哈希類型的內部編碼有兩種:
ziplist(壓縮列表):當哈希類型元素個數小于hash-max-ziplist-entries 配置(默認512個)、同時所有值都小于hash-max-ziplist-value配置(默認64 字節)時,Redis會使用ziplist作為哈希的內部實現,ziplist使用更加緊湊的 結構實現多個元素的連續存儲,所以在節省內存方面比hashtable更加優秀
hashtable(哈希表):當哈希類型無法滿足ziplist的條件時,Redis會使 用hashtable作為哈希的內部實現,因為此時ziplist的讀寫效率會下降,而 hashtable的讀寫時間復雜度為O(1)
演示說明
當field個數比較少且沒有大的value時,內部編碼為ziplist:
當有value大于64字節,內部編碼會由ziplist變為hashtable:
當field個數超過512,內部編碼也會由ziplist變為hashtable
四、字符串和散列的比較與選擇
散列的優點
散列的最大優勢,只需要在數據庫里面創建一個鍵,就可以把任意多的字段和值存儲到散列里面
字符串的優點
雖然散列鍵命令和字符串鍵命令在部分功能上有重合的地方,但是字符串鍵命令提供的操作比散列鍵命令更為豐富。比如,字符串能夠使用 SETRANGE 命令和 GETRANGE 命令設置或者讀取字符 串值的其中一部分,或者使用 APPEND 命令將新內容追加到字符串值的末尾,而散列鍵并不支持 這些操作
再比如我們要設置鍵過期時間,鍵過期時間是針對整個鍵的,用戶無法為散列中的不同字段設置不 同的過期時間,所以當一個散列鍵過期的時候,他包含的所有字段和值都會被刪除。與此相反,如 果用戶使用字符串鍵存儲信息項,就不會遇到這樣的問題——用戶可以為每個字符串鍵分別設置不 同的過期時間,讓它們根據實際的需要自動被刪除
字符串和散列的選擇
使用場景對比:
如果程序需要為單個數據項單獨設置過期的時間,那么使用字符串鍵。
如果程序需要對數據項執行諸如 SETRANGE、GETRANGE 或者 APPEND 等操作,那么優 先考慮使用字符串鍵。當然,用戶也可以選擇把數據存儲在散列中,然后將類似 SETRANG E、GETRANGE 這樣的操作交給客戶端執行
如果程序需要存儲的數據項比較多,并且你希望盡可能地減少存儲數據所需的內存,就應該優 先考慮使用散列鍵
如果多個數據項在邏輯上屬于同一組或者同一類,那么應該優先考慮使用散列鍵
五、使用場景
短網址生成程序
此時我們可以根據該短鏈接查詢到具體的源網址,并記錄點擊次數
存儲信息
下圖為關系型數據表記錄的兩條用戶信息,用戶的屬性作為表的列, 每條用戶信息作為行
如果將其用哈希類型存儲,如下圖所示:
相比于使用字符串序列化緩存用戶信息,哈希類型變得更加直觀,并且在更新操作上會更加便捷。可以將每個用戶的id定義為鍵后綴,多對fieldvalue對應每個用戶的屬性,類似如下偽代碼:
UserInfo?getUserInfo(long?id){//?用戶id作為key后綴userRedisKey?=?"user:info:"?+?id;//?使用hgetall獲取所有用戶信息映射關系userInfoMap?=?redis.hgetAll(userRedisKey);UserInfo?userInfo;if?(userInfoMap?!=?null)?{//?將映射關系轉換為UserInfouserInfo?=?transferMapToUserInfo(userInfoMap);}?else?{//?從MySQL中獲取用戶信息userInfo?=?mysql.get(id);//?將userInfo變為映射關系使用hmset保存到Redis中redis.hmset(userRedisKey,?transferUserInfoToMap(userInfo));//?添加過期時間redis.expire(userRedisKey,?3600);}return?userInfo;}
但是需要注意的是哈希類型和關系型數據庫有兩點不同之處:
哈希類型是稀疏的,而關系型數據庫是完全結構化的,例如哈希類型 每個鍵可以有不同的field,而關系型數據庫一旦添加新的列,所有行都要為 其設置值(即使為NULL),如下圖所示
關系型數據庫可以做復雜的關系查詢,而Redis去模擬關系型復雜查詢 開發困難,維護成本高
三種方案
開發人員需要將兩者的特點搞清楚,才能在適合的場景使用適合的技術。到目前為止,我們已經能夠用三種方法緩存用戶信息,下面給出三種方案的實現方法和優缺點分析
①原生字符串類型:每個屬性一個鍵
優點:簡單直觀,每個屬性都支持更新操作
缺點:占用過多的鍵,內存占用量較大,同時用戶信息內聚性比較差, 所以此種方案一般不會在生產環境使用
set user:1:name tom
set user:1:age 23
set user:1:city beijin
②序列化字符串類型:將用戶信息序列化后用一個鍵保存。
優點:簡化編程,如果合理的使用序列化可以提高內存的使用效率
缺點:序列化和反序列化有一定的開銷,同時每次更新屬性都需要把全 部數據取出進行反序列化,更新后再序列化到Redis中
set user:1 serialize(userInfo)
③哈希類型:每個用戶屬性使用一對field-value,但是只用一個鍵保存
優點:簡單直觀,如果使用合理可以減少內存空間的使用
缺點:要控制哈希在ziplist和hashtable兩種內部編碼的轉換,hashtable會消耗更多內存
hmset user:1 name tomage 23 city beijing
不過對于我而言,我經常用到的是string,方便,快捷,一般是存儲一些玩家的登錄緩存信息,或者一些全服跨服戰斗的相關數據