天天看點

Redis開發與運維. 2.7 鍵管理

<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&gt; get python

"jedis"

下面操作将鍵python重命名為java:

127.0.0.1:6379&gt; set python jedis

ok

127.0.0.1:6379&gt; rename python java

(nil)

127.0.0.1:6379&gt; get java

如果在rename之前,鍵java已經存在,那麼它的值也将被覆寫,如下所示:

127.0.0.1:6379&gt; set a b

127.0.0.1:6379&gt; set c d

127.0.0.1:6379&gt; rename a c

127.0.0.1:6379&gt; get a

127.0.0.1:6379&gt; get c

"b"

為了防止被強行rename,redis提供了renamenx指令,確定隻有newkey不存在時候才被覆寫,例如下面操作renamenx時,newkey=python已經存在,傳回結果是0代表沒有完成重命名,是以鍵java和python的值沒變:

127.0.0.1:6379&gt; set java jedis

127.0.0.1:6379&gt; set python redis-py

127.0.0.1:6379&gt; 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&gt; rename key key

redis 3.2之前的版本會提示錯誤:

(error) err source and destination objects

are the same

2.?随機傳回一個鍵

randomkey

下面示例中,目前資料庫有1000個鍵值對,randomkey指令會随機從中挑選一個鍵:

127.0.0.1:6379&gt; dbsize

1000

127.0.0.1:6379&gt; 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&gt; set hello world

127.0.0.1:6379&gt; expire hello 10

(integer) 1

#還剩7秒

127.0.0.1:6379&gt; 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&gt; 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&gt; expire not_exist_key 30

2)如果過期時間為負值,鍵會立即被删除,猶如使用del指令一樣:

127.0.0.1:6379&gt; expire hello -2

127.0.0.1:6379&gt; get hello

3)persist指令可以将鍵的過期時間清除:

127.0.0.1:6379&gt; hset key f1 v1

127.0.0.1:6379&gt; expire key 50

127.0.0.1:6379&gt; ttl key

(integer) 46

127.0.0.1:6379&gt; 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&gt; 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&gt; set hello world

redis-source&gt; dump hello

"\x00\x05world\x06\x00\x8f&lt;t\x04%\xfcnq"

2)在目标redis上執行restore:

redis-target&gt; get hello

redis-target&gt; 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&gt; migrate 127.0.0.1 6380

hello 0 1000

情況2:源redis和目标redis都有鍵hello:

127.0.0.1:6380&gt; get hello

"redis"

如果migrate指令沒有加replace選項會收到錯誤提示,如果加了replace會傳回ok表明遷移成功:

127.0.0.1:6379&gt; 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&gt; 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&gt; mset hello world redis

best jedis best hill high

如果要擷取所有的鍵,可以使用keys pattern指令:

127.0.0.1:6379&gt; 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&gt; keys [j,r]edis

1) "jedis"

2) "redis"

例如下面操作會比對到hello和hill這兩個鍵:

127.0.0.1:6379&gt; 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&gt; 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&gt; 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&gt; 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 &amp;&amp; elements.size() &gt; 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&gt; set hello world      #預設進到0号資料庫

127.0.0.1:6379&gt; select 15                #切換到15号資料庫

127.0.0.1:6379[15]&gt; 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&gt; select 1

127.0.0.1:6379[1]&gt; dbsize

(integer) 3

如果在0号資料庫執行flushdb,1号資料庫的資料依然還在:

127.0.0.1:6379&gt; flushdb

在任意資料庫執行flushall會将所有資料庫清除:

127.0.0.1:6379&gt; flushall

flushdb/flushall指令可以非常友善的清理資料,但是也帶來兩個問題:

flushdb/flushall指令會将所有資料清除,一旦誤操作後果不堪設想,第12章會介紹rename-command配置規避這個問題,以及如何在誤操作後快速恢複資料。

如果目前資料庫鍵值數量比較多,flushdb/flushall存在阻塞redis的可能性。

是以在使用flushdb/flushall一定要小心謹慎。