雜湊
在Redis中提供了雜湊(hash)型別,雜湊型別是指鍵值本身又是一個鍵值對結構。形如
value={{field1,value1},...,{fieldN,valueN}}
。
Redis的鍵值對和雜湊型別的關係如下圖:
注意:雜湊型別的對映關係叫做field-value
,這裡的value
是指field
對應的值,不是鍵對應的值。
命令
- 設定值
## 命令:hset key filed value
## 例如:為user:1設定一對field-value
172.17.236.250:6379> hset user:1 name tom
(integer) 1
複製程式碼
設定成功則返回1,設定失敗返回0。Redis也提供了hsetnx
,它們的關係就和set
和setnx
一樣,只是作用域由鍵變成了field
。
- 獲取值
## 命令:hget key field
## 例如:獲取user:1的name屬性的值
172.17.236.250:6379> hget user:1 name
"tom"
## 如果鍵,或者field不存在則返回 nil
172.17.236.250:6379> hget user:2 name
(nil)
172.17.236.250:6379> hget user:1 age
(nil)
複製程式碼
- 刪除
field
## 命令:hdel key field \[field ... \]
## 例如:刪除存在的 field name 和不存在的 field age
## hdel 可以刪除一個或多個 field ,返回結果是成功刪除field的個數。
172.17.236.250:6379> hdel user:1 name
(integer) 1
172.17.236.250:6379> hdel user:1 age
(integer) 0
複製程式碼
- 計算
field
的個數
## 命令:hlen key
## 例如:獲取 user:1的 field 的個數
172.17.236.250:6379> hlen user:1
(integer) 1
複製程式碼
- 批量設定或獲取field-value
## 批量獲取命令:hmget key field \[field...\]
## 批量設定命令:hmset key value \[field value ... \]
## 例如:批量設定 user:1 的 field-value
172.17.236.250:6379> hmset user:1 name mike age 12 city tianjin
OK
## 例如:批量獲取 user:1 的 value
172.17.236.250:6379> hmget user:1 name city age
1) "mike"
2) "tianjin"
3) "12"
複製程式碼
- 判斷
field
是否存在
## 命令:hexists key field
## 例如:user:1 包含屬性 name ,所以返回結果 1,否則返回 0
172.17.236.250:6379> hexists user:1 name
(integer) 1
複製程式碼
- 獲取所有
field
## 命令:hkeys key
## 例如:返回指定 hash 鍵所有的 field
172.17.236.250:6379> hkeys user:1
1) "name"
2) "age"
3) "city"
複製程式碼
- 獲取所有的
value
## 命令: hvals key
## 例如:獲取 user:1 全部 value
172.17.236.250:6379> hvals user:1
1) "mike"
2) "12"
3) "tianjin"
複製程式碼
- 獲取所有的
field-value
## 命令:hgetall key
## 例如:獲取 user:1 鍵所有的 field-value
172.17.236.250:6379> hgetall user:1
1) "name"
2) "mike"
3) "age"
4) "12"
5) "city"
6) "tianjin"
複製程式碼
注意:在使用hgetall
時,如果hash
元素個數較多,會存在Redis阻塞的可能。只需獲取部分field
,可以使用 hmget
,若一定要獲取所有的field-value
可以使用hscan
命令,該命令會漸進式遍歷hash
型別。
- 為
hash
表中的屬性field
新增increment
## 命令:hincrby key field increment
## 命令:hincrbyfloat key field increment
## 例如:給 user:1 的 age 做自增操作
172.17.236.250:6379> hmget user:1 age
1) "12"
172.17.236.250:6379> hincrby user:1 age 2
(integer) 14
172.17.236.250:6379> hincrbyfloat user:1 age 6.9
"20.9"
複製程式碼
如果鍵不存在,則建立新的hash
表,並執行hincrby
。如果field
不存在,在執行命令前則屬性的值初始化為0。
- 計算值得字串長度(基於Redis3.2版本及以上)
## 命令:hstrlen key field
複製程式碼
下面兩張圖片是Redis的hash型別命令的時間複雜度,我們們可以根據這兩張圖片的表格並結合實際開發場景選擇合適命令使用。
內部編碼
雜湊型別的內部編碼有兩種:
ziplist
(壓縮列表):當雜湊型別元素個數小於hash-max-ziplist-entries
配置(預設512個)、同時所有的值小於hash-max-ziplist-value
配置(預設64個位元組)時。Redis會使用ziplist
作為雜湊的內部實現,ziplist
使用更加緊湊的結構實現多個元素的連續儲存,所以在節省記憶體方面比hashtable
更加優秀。hashtable
(雜湊表):當雜湊型別無法滿足ziplist的條件時,Redis會使用hashtable
作為雜湊的內部實現,因為此時ziplist
的讀寫效率會下降,而hashtable
的讀寫複雜度為O(1)。
使用場景
下圖為關係型資料庫儲存兩條使用者的記錄資訊:
下圖為使用雜湊型別快取使用者資訊: 相對於使用字串序列化快取使用者資訊,雜湊型別更加直觀,並且在更新操作上更加方便。將每個使用者的id作為鍵字尾,多對field-value
對應每個使用者的屬性。虛擬碼如下:
UserInfo getUserInfo(long id){
// 使用者 id 作為字尾
userRedisKey = "user:info:"+id;
// 使用 hgetall 獲取所有使用者資訊關係對映
userInfoMap = redis.hgetAll(userRedisKey);
UserInfo userInfo;
if(userInfo != 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)。如下圖:
-
關係型資料庫可以做複雜查詢,而Redis模擬關係型資料庫做複雜查詢開發困難,維護成本高。
我們暫時可以使用三種方式快取使用者資訊,下面分析一下這三種方案。
- 原生字串型別:每個屬性一個鍵。
set user:1:name tom
set user:1:age 12
set user:1:city beijing
複製程式碼
優點:簡單直觀、每個屬性都支援更新操作。 缺點:佔用鍵過多,記憶體佔用量大,使用者資訊內聚性差(生產環境不會使用)。
- 序列化字串型別:將使用者資訊序列化後用一個字串儲存。
set user:1 serialize(userInfo)
複製程式碼
優點:簡化程式設計,使用合理可以提高記憶體的使用效率。 缺點:序列化和反序列化有一定開銷,每次更新屬性都需要把全部資料取出反序列化,更新後再序列化放入Redis。
- 雜湊型別:每個使用者屬性使用一對
field-value
,但是隻用一個鍵儲存。
hmset user:1 name tomage 23 city beijing
複製程式碼
優點:簡單直觀,如果使用合理可以減少記憶體空間的使用。
缺點:要控制雜湊在ziplist
和hashtable
兩種內部編碼的轉換,hashtable
會消耗更多記憶體。
列表
列表型別是用來儲存多個有序的字串。如下圖:
列表中的每一個字串稱為元素。在Redis中可以對列表兩端插入(push
)和彈出(pop
),還可以獲取指定範圍的列表、獲取指定索引下標的元素等。如下圖:
列表型別的特點:
- 元素有序。
- 元素可以重複。
命令
列表的五種操作型別,命令如下圖:
- 從右邊插入元素
## 命令:rpush key value \[value ... \]
## 例如:從右向左插入元素 c、b、a
172.17.236.250:6379> rpush listkey c b a
(integer) 3
複製程式碼
- 從左到右獲取全部元素
## 命令:lrange key 0 -1
## 例如:從左到右獲取 listkey 的全部元素
172.17.236.250:6379> lrange listkey 0 -1
1) "c"
2) "b"
3) "a"
複製程式碼
- 從左邊插入元素
## 命令:lpush key value \[value ... \]
複製程式碼
- 在某個元素前或者後插入元素
## 命令:linsert key before | after pivot value
## 例如:在列表的元素 b 前插入 java
172.17.236.250:6379> linsert listkey before b java
(integer) 4
172.17.236.250:6379> lrange listkey 0 -1
1) "c"
2) "java"
3) "b"
4) "a"
複製程式碼
- 獲取指定範圍內的元素列表
## 命令:lrange key start end
## 例如:獲取 listkey 的第二到第四個元素
172.17.236.250:6379> lrange listkey 1 3
1) "java"
2) "b"
3) "a"
複製程式碼
lrange
操作會獲取列表指定索引範圍的所有元素。索引下標從左到右分別是 0 到 N-1,但是從右到左分別是 -1 到 -N,lrange
的end
選項包含了自身。
- 獲取列表指定索引下標元素
## 命令:lindex key index
## 例如:獲取 keylist 的最後一個元素
172.17.236.250:6379> lindex listkey -1
"a"
複製程式碼
- 獲取列表長度
## 命令:llen key
## 例如:獲取 listkey 的長度
172.17.236.250:6379> llen listkey
(integer) 4
複製程式碼
- 從列表左側刪除元素
## 命令:lpop key
## 例如:將 listkey 列表最左側的元素刪除
172.17.236.250:6379> lpop listkey
"c"
172.17.236.250:6379> lrange listkey 0 -1
1) "java"
2) "b"
3) "a"
複製程式碼
- 從列表右側彈出
命令:rpop key
複製程式碼
- 刪除指定元素
命令:lrem key count value
例如:刪除4個為a的元素
172.17.236.250:6379> lpush listkey a a a a a
(integer) 8
172.17.236.250:6379> lrange listkey 0 -1
1) "a"
2) "a"
3) "a"
4) "a"
5) "a"
6) "java"
7) "b"
8) "a"
172.17.236.250:6379> lrem listkey 4 a
(integer) 4
172.17.236.250:6379> lrange listkey 0 -1
1) "a"
2) "java"
3) "b"
4) "a"
複製程式碼
lrem
命令會從列表中找到等於value
的元素進行刪除,根據count
的不同分為三種情況:
1. count > 0
:從左到右,刪除最多 count
個元素
2. count < 0
:從右到左,刪除最多 count
絕對值個元素
3. count = 0
:刪除所有
- 按照索引範圍修剪列表
## 命令:ltrim key start end
## 例如:保留列表 listkey 的第二個到第四個元素
172.17.236.250:6379> ltrim listkey 1 3
OK
172.17.236.250:6379> lrange listkey 0 -1
1) "java"
2) "b"
3) "a"
複製程式碼
- 修改指定索引下標的元素
## 命令:lset key index newValue
## 例如:將列表 listkey 中的第三個元素設定為 python
172.17.236.250:6379> lset listkey 2 python
OK
172.17.236.250:6379> lindex listkey 2
"python"
複製程式碼
- 阻塞式彈出
## 命令:blpop | brpop key \[key ... \] timeout
複製程式碼
blpop
和 brpop
是 lpop
和 rpop
的阻塞版本,它們除了彈出方向不同,使用方法基本相同。以brpop
進行說明。
brpop
包含兩個引數:
- key [key ... ]:多個列表的鍵
- timeout:阻塞時間(單位:秒)
- 列表為空:如果
timeout = 3
,那麼客戶端等3秒後返回,如果timeout = 0
,則一直處於阻塞狀態:
172.17.236.250:6379> brpop list:test 3
(nil)
(3.00s)
172.17.236.250:6379> brpop list:test 0
··· 阻塞 ···
複製程式碼
- 如果在此期間列表新增了元素,客戶端會立刻返回
172.17.236.250:6379> brpop list:test 0
1) "list:test"
2) "element1"
複製程式碼
- 列表不為空時,客戶端會立刻返回
172.17.236.250:6379> brpop listkey 3
1) "listkey"
2) "python"
複製程式碼
注意:
- 如果是多個鍵,那麼
brpop
會從左至右遍歷鍵,一但有一個鍵能彈出元素,客戶端立即返回。 - 如果多個客戶端對同一個鍵執行
brpop
,那麼最先執行brpop
命令的客戶端可以獲取到彈出的值。 下表示列表的命令時間複雜度,可以參考這個表選擇適合的命令。
內部編碼
列表型別的內部編碼有兩種。
ziplist
(壓縮列表): 當列表的元素個數小於list-max-ziplist-entries
配置(預設512個),同時列表中每個元素的值都小於list-max-ziplist-value
配置時(預設64個位元組),Redis會選用ziplist
作為列表的內部實現。linkedlist
(連結串列): 當列表型別無法滿足ziplist的條件時,Redis會使用linkedlist
作為列表的內部實現。
使用場景
- 訊息佇列
Redis的
lpush+brpop
命令組合即可實現阻塞佇列,生產者客戶端使用lpush
從列表左側插入元素,多個消費者客戶端使用brpop
命令阻塞式的搶列表尾部的元素,多個客戶端保證了消費的負載均衡和高可用性。如下圖: - 文章列表 每一個使用者都有自己的文章列表,需要分頁展示文章列表。這時就可以使用列表,因為列表有序且支援按照索引範圍獲取元素。 每一篇文章使用雜湊結構儲存,例如:每篇文章有三個屬性 title、 timestamp、content:
172.17.236.250:6379> hmset acticle:1 title xx timestamp 1476536196 content xxx
OK
172.17.236.250:6379> hmset acticle:1 title yy timestamp 1476536196 content yyy
OK
複製程式碼
向使用者文章列表新增文章,user:{id}:articles
作為使用者文章列表的鍵:
172.17.236.250:6379> lpush user:1:articles article:1
(integer) 1
複製程式碼
分頁獲取使用者的文章列表,虛擬碼如下:
articles = lrange user:1: articles 0 9
for article in {articles}
hgetall {article}
複製程式碼
使用列表型別儲存和獲取文章列表存在兩個問題:
- 每次分頁獲取文章個數多,需要多次執行
hgetall
,此時可以考慮使用 pipeline 批量獲取,或者將文章資料序列化為字串型別,使用 mget 批量獲取。 - 分頁獲取文章列表,
lrange
命令在列表過大的情況下,獲取列表中間的元素效能會變差,此時可以考慮將列表做二級拆分。
在實際場景中列表的使用場景很多,選擇時可以參考一下口訣:
- lpush + lpop = Stack(棧)
- lpush + rpop = Queue(佇列)
- lpush + ltrim = Capped Connection(有限集合)
- lpush + brpop = Message Queue(訊息佇列)