天天看點

Redis開發與運維. 2.2 字元串

<b>2.2 字元串</b>

字元串類型是redis最基礎的資料結構。首先鍵都是字元串類型,而且其他幾種資料結構都是在字元串類型基礎上建構的,是以字元串類型能為其他四種資料結構的學習奠定基礎。如圖2-7所示,字元串類型的值實際可以是字元串(簡單的字元串、複雜的字元串(例如json、xml))、數字(整數、浮點數),甚至是二進制(圖檔、音頻、視訊),但是值最大不能超過512mb。

圖2-7 字元串資料結構

<b>2.2.1 指令</b>

字元串類型的指令比較多,本小節将按照常用和不常用兩個次元進行說明,但是這裡常用和不常用是相對的,希望讀者盡可能都去了解和掌握。

1.?常用指令

(1)設定值

set key value [ex seconds] [px milliseconds]

[nx|xx]

下面操作設定鍵為hello,值為world的鍵值對,傳回結果為ok代表設定成功:

127.0.0.1:6379&gt; set hello world

ok

set指令有幾個選項:

ex seconds:為鍵設定秒級過期時間。

px milliseconds:為鍵設定毫秒級過期時間。

nx:鍵必須不存在,才可以設定成功,用于添加。

xx:與nx相反,鍵必須存在,才可以設定成功,用于更新。

除了set選項,redis還提供了setex和setnx兩個指令:

setex key seconds value

setnx key value

它們的作用和ex和nx選項是一樣的。下面的例子說明了set、setnx、set xx的

差別。

目前鍵hello不存在:

127.0.0.1:6379&gt; exists hello

(integer) 0

設定鍵為hello,值為world的鍵值對:

因為鍵hello已存在,是以setnx失敗,傳回結果為0:

127.0.0.1:6379&gt; setnx hello redis

因為鍵hello已存在,是以set xx成功,傳回結果為ok:

127.0.0.1:6379&gt; set hello jedis xx

setnx和setxx在實際使用中有什麼應用場景嗎 以setnx指令為例子,由于redis的單線程指令處理機制,如果有多個用戶端同時執行setnx key value,根據setnx的特性隻有一個用戶端能設定成功,setnx可以作為分布式鎖的一種實作方案,redis官方給出了使用setnx實作分布式鎖的方法:http://redis.io/topics/distlock。

(2)擷取值

get key

下面操作擷取鍵hello的值:

127.0.0.1:6379&gt; get hello

"world"

如果要擷取的鍵不存在,則傳回nil(空):

127.0.0.1:6379&gt; get not_exist_key

(nil)

(3)批量設定值

mset key value [key value ...]

下面操作通過mset指令一次性設定4個鍵值對:

127.0.0.1:6379&gt; mset a 1 b 2 c 3 d 4

(4)批量擷取值

mget key [key ...]

下面操作批量擷取了鍵a、b、c、d的值:

127.0.0.1:6379&gt; mget a b c d

1) "1"

2) "2"

3) "3"

4) "4"

如果有些鍵不存在,那麼它的值為nil(空),結果是按照傳入鍵的順序傳回:

127.0.0.1:6379&gt; mget a b c f

4) (nil)

批量操作指令可以有效提高開發效率,假如沒有mget這樣的指令,要執行n次get指令需要按照圖2-8的方式來執行,具體耗時如下:

n次get時間 = n次網絡時間 + n次指令時間

圖2-8 n次get指令執行模型

使用mget指令後,要執行n次get指令操作隻需要按照圖2-9的方式來完成,具體耗時如下:

n次get時間 = 1次網絡時間 + n次指令時間

圖2-9 一次mget指令執行模型

redis可以支撐每秒數萬的讀寫操作,但是這指的是redis服務端的處理能力,對于用戶端來說,一次指令除了指令時間還是有網絡時間,假設網絡時間為1毫秒,指令時間為0.1毫秒(按照每秒處理1萬條指令算),那麼執行1000次get指令和1次mget指令的差別如表2-1,因為redis的處理能力已經足夠高,對于開發人員來說,網絡可能會成為性能的瓶頸。

表2-1 1000次get和1次get對比表

操  作         時  間

1?000次get   1?000 × 1 + 1?000 × 0.1 = 1?100毫秒 = 1.1秒

1次met(組裝了1?000個鍵值對)  1 × 1 + 1?000 × 0.1 = 101毫秒 = 0.101秒

學會使用批量操作,有助于提高業務處理效率,但是要注意的是每次批量操作所發送的指令數不是無節制的,如果數量過多可能造成redis阻塞或者網絡擁塞。

(5)計數

incr key

incr指令用于對值做自增操作,傳回結果分為三種情況:

值不是整數,傳回錯誤。

值是整數,傳回自增後的結果。

鍵不存在,按照值為0自增,傳回結果為1。

例如對一個不存在的鍵執行incr操作後,傳回結果是1:

127.0.0.1:6379&gt; exists key

127.0.0.1:6379&gt; incr key

(integer) 1

再次對鍵執行incr指令,傳回結果是2:

(integer) 2

如果值不是整數,那麼會傳回錯誤:

127.0.0.1:6379&gt; incr hello

(error) err value is not an integer or out

of range

除了incr指令,redis提供了decr(自減)、incrby(自增指定數字)、decrby(自減指定數字)、incrbyfloat(自增浮點數):

decr key

incrby key increment

decrby key decrement

incrbyfloat key increment

很多存儲系統和程式設計語言内部使用cas機制實作計數功能,會有一定的cpu開銷,但在redis中完全不存在這個問題,因為redis是單線程架構,任何指令到了redis服務端都要順序執行。

2.?不常用指令

(1)追加值

append key value

append可以向字元串尾部追加值,例如:

127.0.0.1:6379&gt; get key

"redis"

127.0.0.1:6379&gt; append key world

(integer) 10

"redisworld"

(2)字元串長度

strlen key

例如,目前值為redisworld,是以傳回值為10:

127.0.0.1:6379&gt; strlen key

下面操作傳回結果為6,因為每個中文占用3個位元組:

127.0.0.1:6379&gt; set hello "世界"

127.0.0.1:6379&gt; strlen hello

(integer) 6

(3)設定并傳回原值

getset key value

getset和set一樣會設定值,但是不同的是,它同時會傳回鍵原來的值,例如:

127.0.0.1:6379&gt; getset hello world

127.0.0.1:6379&gt; getset hello redis

(4)設定指定位置的字元

setrange key offeset value

下面操作将值由pest變為了best:

127.0.0.1:6379&gt; set redis pest

127.0.0.1:6379&gt; setrange redis 0 b

(integer) 4

127.0.0.1:6379&gt; get redis

"best"

(5)擷取部分字元串

getrange key start end

start和end分别是開始和結束的偏移量,偏移量從0開始計算,例如下面操作擷取了值best的前兩個字元。

127.0.0.1:6379&gt; getrange redis 0 1

"be"

表2-2是字元串類型指令的時間複雜度,開發人員可以參考此表,結合自身業務需求和資料大小選擇适合的指令。

表2-2 字元串類型指令時間複雜度

命  令         時間複雜度

set key value    o(1)

get key     o(1)?

del key [key ...]         o(k),k是鍵的個數

mset key value [key value ...]  o(k),k是鍵的個數

mget key [key ...]     o(k),k是鍵的個數

incr key    o(1)

decr key   o(1)

incrby key increment        o(1)

decrby key decrement     o(1)

incrbyfloat key increment        o(1)

append key value     o(1)

strlen key          o(1)

setrange key offset value         o(1)

getrange key start end    o(n),n是字元串長度,由于擷取字元串非常快,是以如果字元串不是很長,可以視同為o(1)

<b>2.2.2 内部編碼</b>

字元串類型的内部編碼有3種:

int:8個位元組的長整型。

embstr:小于等于39個位元組的字元串。

raw:大于39個位元組的字元串。

redis會根據目前值的類型和長度決定使用哪種内部編碼實作。

整數類型示例如下:

127.0.0.1:6379&gt; set key 8653

127.0.0.1:6379&gt; object encoding key

"int"

短字元串示例如下:

#小于等于39個位元組的字元串:embstr

127.0.0.1:6379&gt; set key

"hello,world"

"embstr"

長字元串示例如下:

#大于39個位元組的字元串:raw

127.0.0.1:6379&gt; set key "one string

greater than 39 byte........."

"raw"

(integer) 40

有關字元串類型的記憶體優化技巧将在8.3節詳細介紹。

<b>2.2.3 典型使用場景</b>

1.?緩存功能

圖2-10是比較典型的緩存使用場景,其中redis作為緩存層,mysql作為存儲層,絕大部分請求的資料都是從redis中擷取。由于redis具有支撐高并發的特性,是以緩存通常能起到加速讀寫和降低後端壓力的作用。

下面僞代碼模拟了圖2-10的通路過程:

1)該函數用于擷取使用者的基礎資訊:

userinfo getuserinfo(long id){

...

}

2)首先從redis擷取使用者資訊:

// 定義鍵

userrediskey = "user:info:" + id;

// 從redis擷取值

value = redis.get(userrediskey);

if (value != null) {

// 将值進行反序列化為userinfo并傳回結果

userinfo = deserialize(value);

return userinfo;

與mysql等關系型資料庫不同的是,redis沒有指令空間,而且也沒有對鍵名有強制要求(除了不能使用一些特殊字元)。但設計合理的鍵名,有利于防止鍵沖突和項目的可維護性,比較推薦的方式是使用“業務名:對象名: id : [屬性]”作為鍵名(也可以不是分号)。例如mysql的資料庫名為vs,使用者表名為user,那麼對應的鍵可以用"vs:user:1","vs:user:1:name"來表示,如果目前redis隻被一個業務使用,甚至可以去掉“vs:”。如果鍵名比較長,例如“user:{uid}:friends:messages:{mid}”,可以在能描述鍵含義的前提下适當減少鍵的長度,例如變為“u:{uid}:fr:m:{mid}”,進而減少由于鍵過長的記憶體浪費。

3)如果沒有從redis擷取到使用者資訊,需要從mysql中進行擷取,并将結果回寫到redis,添加1小時(3600秒)過期時間:

// 從mysql擷取使用者資訊

userinfo = mysql.get(id);

// 将userinfo序列化,并存入redis

redis.setex(userrediskey, 3600,

serialize(userinfo));

// 傳回結果

return userinfo

整個功能的僞代碼如下:

userrediskey = "user:info:" + id

userinfo userinfo;?

    }

else {

if (userinfo != null)

redis.setex(userrediskey, 3600, serialize(userinfo));

2.?計數

許多應用都會使用redis作為計數的基礎工具,它可以實作快速計數、查詢緩存的功能,同時資料可以異步落地到其他資料源。例如筆者所在團隊的視訊播放數系統就是使用redis作為視訊播放數計數的基礎元件,使用者每播放一次視訊,相應的視訊播放數就會自增1:

long incrvideocounter(long id) {

key = "video:playcount:" + id;

return redis.incr(key);

實際上一個真實的計數系統要考慮的問題會很多:防作弊、按照不同次元計數,資料持久化到底層資料源等。

3.?共享session

如圖2-11所示,一個分布式web服務将使用者的session資訊(例如使用者登入資訊)儲存在各自伺服器中,這樣會造成一個問題,出于負載均衡的考慮,分布式服務會将使用者的通路均衡到不同伺服器上,使用者重新整理一次通路可能會發現需要重新登入,這個問題是使用者無法容忍的。

圖2-11 session分散管理

為了解決這個問題,可以使用redis将使用者的session進行集中管理,如圖2-12所示,在這種模式下隻要保證redis是高可用和擴充性的,每次使用者更新或者查詢登入資訊都直接從redis中集中擷取。

4.?限速

很多應用出于安全的考慮,會在每次進行登入時,讓使用者輸入手機驗證碼,進而确定是否是使用者本人。但是為了短信接口不被頻繁通路,會限制使用者每分鐘擷取驗證碼的頻率,例如一分鐘不能超過5次,如圖2-13所示。

圖2-13 短信驗證碼限速

此功能可以使用redis來實作,下面的僞代碼給出了基本實作思路:

phonenum = "138xxxxxxxx";

key = "shortmsg:limit:" +

phonenum;

// set key value ex 60 nx

isexists = redis.set(key,1,"ex

60","nx");

if(isexists != null || redis.incr(key)

&lt;=5){

// 通過

}else{

// 限速

上述就是利用redis實作了限速功能,例如一些網站限制一個ip位址不能在一秒鐘之内通路超過n次也可以采用類似的思路。

除了上面介紹的幾種使用場景,字元串還有非常多的适用場景,開發人員可以結合字元串提供的相應指令充分發揮自己的想象力。

繼續閱讀