<b>2.7 鍵管理</b>
本節将按照單個鍵、周遊鍵、資料庫管理三個次元對一些通用指令進行介紹。
<b>2.7.1 單個鍵管理</b>
針對單個鍵的指令,前面幾節已經介紹過一部分了,例如type、del、object、exists、expire等,下面将介紹剩餘的幾個重要指令。
1.?鍵重命名
rename key newkey
例如現有一個鍵值對,鍵為python,值為jedis:
127.0.0.1:6379> get python
"jedis"
下面操作将鍵python重命名為java:
127.0.0.1:6379> set python jedis
ok
127.0.0.1:6379> rename python java
(nil)
127.0.0.1:6379> get java
如果在rename之前,鍵java已經存在,那麼它的值也将被覆寫,如下所示:
127.0.0.1:6379> set a b
127.0.0.1:6379> set c d
127.0.0.1:6379> rename a c
127.0.0.1:6379> get a
127.0.0.1:6379> get c
"b"
為了防止被強行rename,redis提供了renamenx指令,確定隻有newkey不存在時候才被覆寫,例如下面操作renamenx時,newkey=python已經存在,傳回結果是0代表沒有完成重命名,是以鍵java和python的值沒變:
127.0.0.1:6379> set java jedis
127.0.0.1:6379> set python redis-py
127.0.0.1:6379> renamenx java python
(integer) 0
"redis-py"
在使用重命名指令時,有兩點需要注意:
由于重命名鍵期間會執行del指令删除舊的鍵,如果鍵對應的值比較大,會存在阻塞redis的可能性,這點不要忽視。
如果rename和renamenx中的key和newkey如果是相同的,在redis 3.2和之前版本傳回結果略有不同。
redis 3.2中會傳回ok:
127.0.0.1:6379> rename key key
redis 3.2之前的版本會提示錯誤:
(error) err source and destination objects
are the same
2.?随機傳回一個鍵
randomkey
下面示例中,目前資料庫有1000個鍵值對,randomkey指令會随機從中挑選一個鍵:
127.0.0.1:6379> dbsize
1000
127.0.0.1:6379> randomkey
"hello"
3.?鍵過期
2.1節簡單介紹鍵過期功能,它可以自動将帶有過期時間的鍵删除,在許多應用場景都非常有幫助。除了expire、ttl指令以外,redis還提供了expireat、pexpire、pexpireat、pttl、persist等一系列指令,下面分别進行說明:
expire key seconds:鍵在seconds秒後過期。
expireat key timestamp:鍵在秒級時間戳timestamp後過期。
下面為鍵hello設定了10秒的過期時間,然後通過ttl觀察它的過期剩餘時間(機關:秒),随着時間的推移,ttl逐漸變小,最終變為-2:
127.0.0.1:6379> set hello world
127.0.0.1:6379> expire hello 10
(integer) 1
#還剩7秒
127.0.0.1:6379> ttl hello
(integer) 7
...
#還剩0秒
#傳回結果為-2,說明鍵hello已經被删除
(integer) -2
ttl指令和pttl都可以查詢鍵的剩餘過期時間,但是pttl精度更高可以達到毫秒級别,有3種傳回值:
大于等于0的整數:鍵剩餘的過期時間(ttl是秒,pttl是毫秒)。
-1:鍵沒有設定過期時間。
-2:鍵不存在。
expireat指令可以設定鍵的秒級過期時間戳,例如如果需要将鍵hello在2016-08-01 00:00:00(秒級時間戳為1469980800)過期,可以執行如下操作:
127.0.0.1:6379> expireat hello
1469980800
除此之外,redis 2.6版本後提供了毫秒級的過期方案:
pexpire key milliseconds:鍵在milliseconds毫秒後過期。
pexpireat key milliseconds-timestamp 鍵在毫秒級時間戳timestamp後過期。
但無論是使用過期時間還是時間戳,秒級還是毫秒級,在redis内部最終使用的都是pexpireat。
在使用redis相關過期指令時,需要注意以下幾點。
1)如果expire key的鍵不存在,傳回結果為0:
127.0.0.1:6379> expire not_exist_key 30
2)如果過期時間為負值,鍵會立即被删除,猶如使用del指令一樣:
127.0.0.1:6379> expire hello -2
127.0.0.1:6379> get hello
3)persist指令可以将鍵的過期時間清除:
127.0.0.1:6379> hset key f1 v1
127.0.0.1:6379> expire key 50
127.0.0.1:6379> ttl key
(integer) 46
127.0.0.1:6379> persist key
(integer) -1
4)對于字元串類型鍵,執行set指令會去掉過期時間,這個問題很容易在開發中被忽視。
如下是redis源碼中,set指令的函數setkey,可以看到最後執行了removeexpire
(db,key)函數去掉了過期時間:
void setkey(redisdb *db, robj *key, robj
*val) {
if (lookupkeywrite(db,key) == null) {
dbadd(db,key,val);
}
else {
dboverwrite(db,key,val);
incrrefcount(val);
// 去掉過期時間
removeexpire(db,key);
signalmodifiedkey(db,key);
}
下面的例子證明了set會導緻過期時間失效,因為ttl變為-1:
127.0.0.1:6379> expire hello 50
5)redis不支援二級資料結構(例如哈希、清單)内部元素的過期功能,例如不能對清單類型的一個元素做過期時間設定。
6)setex指令作為set +
expire的組合,不但是原子執行,同時減少了一次網絡通訊的時間。
有關redis鍵過期的詳細原理,8.2節會深入剖析。
4.?遷移鍵
遷移鍵功能非常重要,因為有時候我們隻想把部分資料由一個redis遷移到另一個redis(例如從生産環境遷移到測試環境),redis發展曆程中提供了move、dump + restore、migrate三組遷移鍵的方法,它們的實作方式以及使用的場景不太相同,下面分别介紹。
(1)move
move key db
如圖2-26所示,move指令用于在redis内部進行資料遷移,redis内部可以有多個資料庫,由于多個資料庫功能後面會進行介紹,這裡隻需要知道redis内部可以有多個資料庫,彼此在資料上是互相隔離的,move key db就是把指定的鍵從源資料庫移動到目标資料庫中,但筆者認為多資料庫功能不建議在生産環境使用,是以這個指令讀者知道即可。
(2)dump + restore
dump key
restore key ttl value
dump + restore可以實作在不同的redis執行個體之間進行資料遷移的功能,整個遷移的過程分為兩步:
1)在源redis上,dump指令會将鍵值序列化,格式采用的是rdb格式。
2)在目标redis上,restore指令将上面序列化的值進行複原,其中ttl參數代表過期時間,如果ttl=0代表沒有過期時間。
整個過程如圖2-27所示。
有關dump + restore有兩點需要注意:第一,整個遷移過程并非原子性的,而是通過用戶端分步完成的。第二,遷移過程是開啟了兩個用戶端連接配接,是以dump的結果不是在源redis和目标redis之間進行傳輸,下面用一個例子示範完整過程。
1)在源redis上執行dump:
redis-source> set hello world
redis-source> dump hello
"\x00\x05world\x06\x00\x8f<t\x04%\xfcnq"
2)在目标redis上執行restore:
redis-target> get hello
redis-target> restore hello 0
"world"
上面2步對應的僞代碼如下:
redis sourceredis = new
redis("sourcemachine", 6379);
redis targetredis = new
redis("targetmachine", 6379);
targetredis.restore("hello", 0,
sourceredis.dump(key));
(3)migrate
migrate host port key|""
destination-db timeout [copy] [replace] [keys key [key ...]]
migrate指令也是用于在redis執行個體間進行資料遷移的,實際上migrate指令就是将dump、restore、del三個指令進行組合,進而簡化了操作流程。migrate指令具有原子性,而且從redis 3.0.6版本以後已經支援遷移多個鍵的功能,有效地提高了遷移效率,migrate在10.4節水準擴容中起到重要作用。
整個過程如圖2-28所示,實作過程和dump + restore基本類似,但是有3點不太相同:第一,整個過程是原子執行的,不需要在多個redis執行個體上開啟用戶端的,隻需要在源redis上執行migrate指令即可。第二,migrate指令的資料傳輸直接在源redis和目标redis上完成的。第三,目标redis完成restore後會發送ok給源redis,源redis接收後會根據migrate對應的選項來決定是否在源redis上删除對應的鍵。
下面對migrate的參數進行逐個說明:
host:目标redis的ip位址。
port:目标redis的端口。
key|"":在redis 3.0.6版本之前,migrate隻支援遷移一個鍵,是以此處是要遷移的鍵,但redis 3.0.6版本之後支援遷移多個鍵,如果目前需要遷移多個鍵,此處為空字元串""。
destination-db:目标redis的資料庫索引,例如要遷移到0号資料庫,這裡就寫0。
timeout:遷移的逾時時間(機關為毫秒)。
[copy]:如果添加此選項,遷移後并不删除源鍵。
[replace]:如果添加此選項,migrate不管目标redis是否存在該鍵都會正常遷移進行資料覆寫。
[keys key [key ...]]:遷移多個鍵,例如要遷移key1、key2、key3,此處填寫“keys
key1 key2 key3”。
下面用示例示範migrate指令,為了友善示範源redis使用6379端口,目标redis使用6380端口,現要将源redis的鍵hello遷移到目标redis中,會分為如下幾種情況:
情況1:源redis有鍵hello,目标redis沒有:
127.0.0.1:6379> migrate 127.0.0.1 6380
hello 0 1000
情況2:源redis和目标redis都有鍵hello:
127.0.0.1:6380> get hello
"redis"
如果migrate指令沒有加replace選項會收到錯誤提示,如果加了replace會傳回ok表明遷移成功:
127.0.0.1:6379> migrate 127.0.0.1 6379
(error) err target instance replied with
error: busykey target key name already exists.
hello 0 1000 replace
情況3:源redis沒有鍵hello。如下所示,此種情況會收到nokey的提示:
nokey
下面示範一下redis 3.0.6版本以後遷移多個鍵的功能。
源redis批量添加多個鍵:
127.0.0.1:6379> mset key1 value1 key2
value2 key3 value3
源redis執行如下指令完成多個鍵的遷移:
"" 0 5000 keys key1 key2 key3
至此有關redis資料遷移的指令介紹完了,最後使用表2-9總結一下move、dump + restore、migrate三種遷移方式的異同點,筆者建議使用migrate指令進行鍵值遷移。
表2-9 move、dump + restore、migrate三個指令比較
命 令 作用域 原子性 支援多個鍵
move redis執行個體内部 是 否
dump + restore redis執行個體之間 否 否
migrate redis執行個體之間 是 是
<b>2.7.2 周遊鍵</b>
redis提供了兩個指令周遊所有的鍵,分别是keys和scan,本節将對它們介紹并簡要分析。
1.全量周遊鍵
keys pattern
本章開頭介紹keys指令的簡單使用,實際上keys指令是支援pattern比對的,例如向一個空的redis插入4個字元串類型的鍵值對。
127.0.0.1:6379> mset hello world redis
best jedis best hill high
如果要擷取所有的鍵,可以使用keys pattern指令:
127.0.0.1:6379> keys *
1) "hill"
2) "jedis"
3) "redis"
4) "hello"
上面為了周遊所有的鍵,pattern直接使用星号,這是因為pattern使用的是glob風格的通配符:
*代表比對任意字元。
?代表比對一個字元。
[]代表比對部分字元,例如[1,3]代表比對1,3,[1-10]代表比對1到10的任意數字。
\x用來做轉義,例如要比對星号、問号需要進行轉義。
下面操作比對以j,r開頭,緊跟edis字元串的所有鍵:
127.0.0.1:6379> keys [j,r]edis
1) "jedis"
2) "redis"
例如下面操作會比對到hello和hill這兩個鍵:
127.0.0.1:6379> keys h?ll*
2) "hello"
當需要周遊所有鍵時(例如檢測過期或閑置時間、尋找大對象等),keys是一個很有幫助的指令,例如想删除所有以video字元串開頭的鍵,可以執行如下操作:
redis-cli keys video* | xargs redis-cli del
但是如果考慮到redis的單線程架構就不那麼美妙了,如果redis包含了大量的鍵,執行keys指令很可能會造成redis阻塞,是以一般建議不要在生産環境下使用keys指令。但有時候确實有周遊鍵的需求該怎麼辦,可以在以下三種情況使用:
在一個不對外提供服務的redis從節點上執行,這樣不會阻塞到用戶端的請求,但是會影響到主從複制,有關主從複制我們将在第6章進行詳細介紹。
如果确認鍵值總數确實比較少,可以執行該指令。
使用下面要介紹的scan指令漸進式的周遊所有鍵,可以有效防止阻塞。
2.?漸進式周遊
redis從2.8版本後,提供了一個新的指令scan,它能有效的解決keys指令存在的問題。和keys指令執行時會周遊所有鍵不同,scan采用漸進式周遊的方式來解決keys指令可能帶來的阻塞問題,每次scan指令的時間複雜度是o(1),但是要真正實作keys的功能,需要執行多次scan。redis存儲鍵值對實際使用的是hashtable的資料結構,其簡化模型如圖2-29所示。
那麼每次執行scan,可以想象成隻掃描一個字典中的一部分鍵,直到将字典中的所有鍵周遊完畢。scan的使用方法如下:
scan cursor [match pattern] [count number]
cursor是必需參數,實際上cursor是一個遊标,第一次周遊從0開始,每次scan周遊完都會傳回目前遊标的值,直到遊标值為0,表示周遊結束。
match pattern是可選參數,它的作用的是做模式的比對,這點和keys的模式比對很像。
count number是可選參數,它的作用是表明每次要周遊的鍵個數,預設值是10,此參數可以适當增大。
現有一個redis有26個鍵(英文26個字母),現在要周遊所有的鍵,使用scan指令效果的操作如下。第一次執行scan 0,傳回結果分為兩個部分:第一個部分6就是下次scan需要的cursor,第二個部分是10個鍵:
127.0.0.1:6379> scan 0
1) "6"
2)
1) "w"
2) "i"
3) "e"
4) "x"
5) "j"
6) "q"
7) "y"
8) "u"
9) "b"
10) "o"
使用新的cursor="6",執行scan 6:
127.0.0.1:6379> scan 6
1) "11"
1) "h"
2) "n"
3) "m"
4) "t"
5) "c"
6) "d"
7) "g"
8) "p"
9) "z"
10) "a"
這次得到的cursor="11",繼續執行scan 11得到結果cursor變為0,說明所有的鍵已經被周遊過了:
127.0.0.1:6379> scan 11
1) "0"
1) "s"
2) "f"
3) "r"
4) "v"
5) "k"
6) "l"
除了scan以外,redis提供了面向哈希類型、集合類型、有序集合的掃描周遊指令,解決諸如hgetall、smembers、zrange可能産生的阻塞問題,對應的指令分别是hscan、sscan、zscan,它們的用法和scan基本類似,下面以sscan為例子進行說明,目前集合有兩種類型的元素,例如分别以old:user和new:user開頭,先需要将old:user開頭的元素全部删除,可以參考如下僞代碼:
string key = "myset";
// 定義pattern
string pattern = "old:user*";
// 遊标每次從0開始
string cursor = "0";
while (true) {
// 擷取掃描結果
scanresult scanresult = redis.sscan(key, cursor, pattern);
list elements = scanresult.getresult();
if (elements != null && elements.size() > 0) {
// 批量删除
redis.srem(key, elements);
// 擷取新的遊标
cursor = scanresult.getstringcursor();
// 如果遊标為0表示周遊結束
if ("0".equals(cursor)) {
break;
漸進式周遊可以有效的解決keys指令可能産生的阻塞問題,但是scan并非完美無瑕,如果在scan的過程中如果有鍵的變化(增加、删除、修改),那麼周遊效果可能會碰到如下問題:新增的鍵可能沒有周遊到,周遊出了重複的鍵等情況,也就是說scan并不能保證完整的周遊出來所有的鍵,這些是我們在開發時需要考慮的。
<b>2.7.3 資料庫管理</b>
redis提供了幾個面向redis資料庫的操作,它們分别是dbsize、select、flushdb/flushall指令,本節将通過具體的使用場景介紹這些指令。
1.?切換資料庫
select dbindex
許多關系型資料庫,例如mysql支援在一個執行個體下有多個資料庫存在的,但是與關系型資料庫用字元來區分不同資料庫名不同,redis隻是用數字作為多個資料庫的實作。redis預設配置中是有16個資料庫:
databases 16
假設databases=16,select 0操作将切換到第一個資料庫,select 15選擇最後一個資料庫,但是0号資料庫和15号資料庫之間的資料沒有任何關聯,甚至可以存在相同的鍵:
127.0.0.1:6379> set hello world #預設進到0号資料庫
127.0.0.1:6379> select 15 #切換到15号資料庫
127.0.0.1:6379[15]> get hello #因為15号資料庫和0号資料庫是隔離的,是以get
hello為空
圖2-30更加生動地表現出上述操作過程。同時可以看到,當使用redis-cli -h {ip} -p {port}連接配接redis時,預設使用的就是0号資料庫,當選擇其他資料庫時,會有[index]的字首辨別,其中index就是資料庫的索引下标。
圖2-30 使用select指令切換資料庫
那麼能不能像使用測試資料庫和正式資料庫一樣,把正式的資料放在0号資料庫,測試的資料庫放在1号資料庫,那麼兩者在資料上就不會彼此受影響了。事實真有那麼好嗎
redis3.0中已經逐漸弱化這個功能,例如redis的分布式實作redis
cluster隻允許使用0号資料庫,隻不過為了向下相容老版本的資料庫功能,該功能沒有完全廢棄掉,下面分析一下為什麼要廢棄掉這個“優秀”的功能呢
總結起來有三點:
redis是單線程的。如果使用多個資料庫,那麼這些資料庫仍然是使用一個cpu,彼此之間還是會受到影響的。
多資料庫的使用方式,會讓調試和運維不同業務的資料庫變的困難,假如有一個慢查詢存在,依然會影響其他資料庫,這樣會使得别的業務方定位問題非常的困難。
部分redis的用戶端根本就不支援這種方式。即使支援,在開發的時候來回切換數字形式的資料庫,很容易弄亂。
筆者建議如果要使用多個資料庫功能,完全可以在一台機器上部署多個redis執行個體,彼此用端口來做區分,因為現代計算機或者伺服器通常是有多個cpu的。這樣既保證了業務之間不會受到影響,又合理地使用了cpu資源。
2.?flushdb/flushall
flushdb/flushall指令用于清除資料庫,兩者的差別的是flushdb隻清除目前資料庫,flushall會清除所有資料庫。
例如目前0号資料庫有四個鍵值對、1号資料庫有三個鍵值對:
(integer) 4
127.0.0.1:6379> select 1
127.0.0.1:6379[1]> dbsize
(integer) 3
如果在0号資料庫執行flushdb,1号資料庫的資料依然還在:
127.0.0.1:6379> flushdb
在任意資料庫執行flushall會将所有資料庫清除:
127.0.0.1:6379> flushall
flushdb/flushall指令可以非常友善的清理資料,但是也帶來兩個問題:
flushdb/flushall指令會将所有資料清除,一旦誤操作後果不堪設想,第12章會介紹rename-command配置規避這個問題,以及如何在誤操作後快速恢複資料。
如果目前資料庫鍵值數量比較多,flushdb/flushall存在阻塞redis的可能性。
是以在使用flushdb/flushall一定要小心謹慎。