天天看點

Redis開發與運維. 3.4 事務與Lua

<b>3.4 事務與lua</b>

為了保證多條指令組合的原子性,redis提供了簡單的事務功能以及內建lua腳本來解決這個問題。本節首先簡單介紹redis中事務的使用方法以及它的局限性,之後重點介紹lua語言的基本使用方法,以及如何将redis和lua腳本進行內建,最後給出redis管理lua腳本的相關指令。

<b>3.4.1 事務</b>

熟悉關系型資料庫的讀者應該對事務比較了解,簡單地說,事務表示一組動作,要麼全部執行,要麼全部不執行。例如在社交網站上使用者a關注了使用者b,那麼需要在使用者a的關注表中加入使用者b,并且在使用者b的粉絲表中添加使用者a,這兩個行為要麼全部執行,要麼全部不執行,否則會出現資料不一緻的情況。

redis提供了簡單的事務功能,将一組需要一起執行的指令放到multi和exec兩個指令之間。multi指令代表事務開始,exec指令代表事務結束,它們之間的指令是原子順序執行的,例如下面操作實作了上述使用者關注問題。

127.0.0.1:6379&gt; multi

ok

127.0.0.1:6379&gt; sadd user:a:follow

user:b

queued

127.0.0.1:6379&gt; sadd user:b:fans user:a

可以看到sadd指令此時的傳回結果是queued,代表指令并沒有真正執行,而是暫時儲存在redis中。如果此時另一個用戶端執行sismember user:a:follow user:b傳回結果應該為0。

127.0.0.1:6379&gt; sismember user:a:follow

(integer) 0

隻有當exec執行後,使用者a關注使用者b的行為才算完成,如下所示傳回的兩個結果對應sadd指令。

127.0.0.1:6379&gt; exec

1) (integer) 1

2) (integer) 1

(integer) 1

如果要停止事務的執行,可以使用discard指令代替exec指令即可。

127.0.0.1:6379&gt; discard

如果事務中的指令出現錯誤,redis的處理機制也不盡相同。

1??指令錯誤

例如下面操作錯将set寫成了sett,屬于文法錯誤,會造成整個事務無法執行,key和counter的值未發生變化:

127.0.0.1:6388&gt; mget key counter

1) "hello"

2) "100"

127.0.0.1:6388&gt; multi

127.0.0.1:6388&gt; sett key world

(error) err unknown command 'sett'

127.0.0.1:6388&gt; incr counter

127.0.0.1:6388&gt; exec

(error) execabort transaction discarded

because of previous errors.

2.?運作時錯誤

例如使用者b在添加粉絲清單時,誤把sadd指令寫成了zadd指令,這種就是運作時指令,因為文法是正确的:

127.0.0.1:6379&gt; zadd user:b:fans 1

user:a

2) (error) wrongtype operation against a

key holding the wrong kind of value

可以看到redis并不支援復原功能,sadd user:a:follow user:b指令已經執行成功,開發人員需要自己修複這類問題。

有些應用場景需要在事務之前,確定事務中的key沒有被其他用戶端修改過,才執行事務,否則不執行(類似樂觀鎖)。redis提供了watch指令來解決這類問題,表3-2展示了兩個用戶端執行指令的時序。

表3-2 事務中watch指令示範時序

時間點     用戶端-1 用戶端-2

t1     set

key "java" 

t2     watch

key        

t3     multi        

t4              append

key python

t5     append

key jedis     

t6     exec

t7     get

key    

可以看到“用戶端-1”在執行multi之前執行了watch指令,“用戶端-2”在“用戶端-1”執行exec之前修改了key值,造成事務沒有執行(exec結果為nil),整個代碼如下所示:

#t1:用戶端1

127.0.0.1:6379&gt; set key "java"

#t2:用戶端1

127.0.0.1:6379&gt; watch key

#t3:用戶端1

#t4:用戶端2

127.0.0.1:6379&gt; append key python

(integer) 11

#t5:用戶端1

127.0.0.1:6379&gt; append key jedis

#t6:用戶端1

(nil)

#t7:用戶端1

127.0.0.1:6379&gt; get key

"javapython"

redis提供了簡單的事務,之是以說它簡單,主要是因為它不支援事務中的復原特性,同時無法實作指令之間的邏輯關系計算,當然也展現了redis的“keep it simple”的特性,下一小節介紹的lua腳本同樣可以實作事務的相關功能,但是功能要強大很多。

<b>3.4.2 lua用法簡述</b>

lua語言是在1993年由巴西一個大學研究小組發明,其設計目标是作為嵌入式程式移植到其他應用程式,它是由c語言實作的,雖然簡單小巧但是功能強大,是以許多應用都選用它作為腳本語言,尤其是在遊戲領域,例如大名鼎鼎的暴雪公司将lua語言引入到“魔獸世界”這款遊戲中,rovio公司将lua語言作為“憤怒的小鳥”這款火爆遊戲的關卡更新引

擎,web伺服器nginx将lua語言作為擴充,增強自身功能。redis将lua作為腳本語言可幫助開發者定制自己的redis指令,在這之前,必須修改源碼。在介紹如何在redis中使用lua腳本之前,有必要對lua語言的使用做一個基本的介紹。

1.?資料類型及其邏輯處理

lua語言提供了如下幾種資料類型:booleans(布爾)、numbers(數值)、strings(字元串)、tables(表格),和許多進階語言相比,相對簡單。下面将結合例子對lua的基本資料類型和邏輯處理進行說明。

(1)字元串

下面定義一個字元串類型的資料:

local strings val = "world"

其中,local代表val是一個局部變量,如果沒有local代表是全局變量。print函數可以列印出變量的值,例如下面代碼将列印world,其中"--"是lua語言的注釋。

-- 結果是"world"

print(hello)

(2)數組

在lua中,如果要使用類似數組的功能,可以用tables類型,下面代碼使用定義了一個tables類型的變量myarray,但和大多數程式設計語言不同的是,lua的數組下标從1開始計算:

local tables myarray = {"redis",

"jedis", true, 88.0}

--true

print(myarray[3])

如果想周遊這個數組,可以使用for和while,這些關鍵字和許多程式設計語言是一緻的。

(a)for

下面代碼會計算1到100的和,關鍵字for以end作為結束符:

local int sum = 0

for i = 1, 100?

do

sum = sum + i

end

-- 輸出結果為5050

print(sum)

要周遊myarray,首先需要知道tables的長度,隻需要在變量前加一個#号即可:

for i = 1, #myarray?

print(myarray[i])

除此之外,lua還提供了内置函數ipairs,使用for index, value ipairs

(tables)可以周遊出所有的索引下标和值:

for index,value in ipairs(myarray)

print(index)

print(value)

(b)while

下面代碼同樣會計算1到100的和,隻不過使用的是while循環,while循環同樣以end作為結束符。

local int i = 0

while i &lt;= 100

sum = sum +i

    i

= i + 1

--輸出結果為5050

(c)if else

要确定數組中是否包含了jedis,有則列印true,注意if以end結尾,if後緊跟then:

if myarray[i] == "jedis"

then

print("true")

break

else

--do nothing

(3)哈希

如果要使用類似哈希的功能,同樣可以使用tables類型,例如下面代碼定義了一個tables,每個元素包含了key和value,其中strings1 .. string2是将兩個字元串進行連接配接:

local tables user_1 = {age = 28, name =

"tome"}

--user_1 age is 28

print("user_1 age is " ..

user_1["age"])

如果要周遊user_1,可以使用lua的内置函數pairs:

for key,value in pairs(user_1)

do print(key .. value)

2.函數定義

在lua中,函數以function開頭,以end結尾,funcname是函數名,中間部分是函數體:

function funcname()

...

contact函數将兩個字元串拼接:

function contact(str1, str2)

return str1 .. str2

--"hello world"

print(contact("hello ",

"world"))

本書隻是介紹了lua部分功能,因為lua的全部功能已經超出本書的範圍,讀者可以購買相應的書籍或者到lua的官方網站(http://www.lua.org/)進行學習。

3.4.3 redis與lua

1.?在redis中使用lua

在redis中執行lua腳本有兩種方法:eval和evalsha。

(1)eval

eval 腳本内容 key個數 key清單 參數清單

下面例子使用了key清單和參數清單來為lua腳本提供更多的靈活性:

127.0.0.1:6379&gt; eval 'return "hello

" .. keys[1] .. argv[1]' 1 redis world

"hello redisworld"

此時keys[1]="redis",argv[1]="world",是以最終的傳回結果是"hello

redisworld"。

如果lua腳本較長,還可以使用redis-cli--eval直接執行檔案。

eval指令和--eval參數本質是一樣的,用戶端如果想執行lua腳本,首先在用戶端編寫好lua腳本代碼,然後把腳本作為字元串發送給服務端,服務端會将執行結果傳回給用戶端,整個過程如圖3-7所示。

圖3-7 eval指令執行lua腳本過程

(2)evalsha

除了使用eval,redis還提供了evalsha指令來執行lua腳本。如圖3-8所示,首先要将lua腳本加載到redis服務端,得到該腳本的sha1校驗和,evalsha指令使用sha1作為參數可以直接執行對應lua腳本,避免每次發送lua腳本的開銷。這樣用戶端就不需要每次執行腳本内容,而腳本也會常駐在服務端,腳本功能得到了複用。

圖3-8 使用evalsha執行lua腳本過程

加載腳本:script load指令可以将腳本内容加載到redis記憶體中,例如下面将lua_get.lua加載到redis中,得到sha1為:"7413dc2440db1fea7c0a0bde841fa68eefaf149c"

# redis-cli script load "$(cat

lua_get.lua)"

"7413dc2440db1fea7c0a0bde841fa68eefaf149c"

執行腳本:evalsha的使用方法如下,參數使用sha1值,執行邏輯和eval一緻。

evalsha 腳本sha1值 key個數 key清單 參數清單

是以隻需要執行如下操作,就可以調用lua_get.lua腳本:

127.0.0.1:6379&gt; evalsha

7413dc2440db1fea7c0a0bde841fa68eefaf149c 1 redis world

2.?lua的redis api

lua可以使用redis.call函數實作對redis的通路,例如下面代碼是lua使用redis.call調用了redis的set和get操作:

redis.call("set",

"hello", "world")

redis.call("get",

"hello")

放在redis的執行效果如下:

127.0.0.1:6379&gt; eval 'return

redis.call("get", keys[1])' 1 hello

"world"

除此之外lua還可以使用redis.pcall函數實作對redis的調用,redis.call和redis.pcall的不同在于,如果redis.call執行失敗,那麼腳本執行結束會直接傳回錯誤,而redis.pcall會忽略錯誤繼續執行腳本,是以在實際開發中要根據具體的應用場景進行函數的選擇。

lua可以使用redis.log函數将lua腳本的日志輸出到redis的日志檔案中,但是一定要控制日志級别。

redis 3.2提供了lua script

debugger功能用來調試複雜的lua腳本,具體可以參考:http://redis.io/topics/ldb。

<b>3.4.4 案例</b>

lua腳本功能為redis開發和運維人員帶來如下三個好處:

lua腳本在redis中是原子執行的,執行過程中間不會插入其他指令。

lua腳本可以幫助開發和運維人員創造出自己定制的指令,并可以将這些指令常駐在redis記憶體中,實作複用的效果。

lua腳本可以将多條指令一次性打包,有效地減少網絡開銷。

下面以一個例子說明lua腳本的使用,目前清單記錄着熱門使用者的id,假設這個清單有5個元素,如下所示:

127.0.0.1:6379&gt; lrange hot:user:list 0

-1

1) "user:1:ratio"

2) "user:8:ratio"

3) "user:3:ratio"

4) "user:99:ratio"

5) "user:72:ratio"

user:{id}:ratio代表使用者的熱度,它本身又是一個字元串類型的鍵:

127.0.0.1:6379&gt; mget user:1:ratio

user:8:ratio user:3:ratio user:99:ratio

user:72:ratio

1) "986"

2) "762"

3) "556"

4) "400"

5) "101"

現要求将清單内所有的鍵對應熱度做加1操作,并且保證是原子執行,此功能可以利用lua腳本來實作。

1)将清單中所有元素取出,指派給mylist:

local mylist =

redis.call("lrange", keys[1], 0, -1)

2)定義局部變量count=0,這個count就是最後incr的總次數:

local count = 0

3)周遊mylist中所有元素,每次做完count自增,最後傳回count:

for index,key in ipairs(mylist)

redis.call("incr",key)

count = count + 1

return count

将上述腳本寫入lrange_and_mincr.lua檔案中,并執行如下操作,傳回結果為5。

redis-cli --eval lrange_and_mincr.lua  hot:user:list

(integer) 5

執行後所有使用者的熱度自增1:

1) "987"

2) "763"

3) "557"

4) "401"

5) "102"

本節給出的隻是一個簡單的例子,在實際開發中,開發人員可以發揮自己的想象力創造出更多新的指令。

<b>3.4.5 redis如何管理lua腳本</b>

redis提供了4個指令實作對lua腳本的管理,下面分别介紹。

(1)script load

script load script

此指令用于将lua腳本加載到redis記憶體中,前面已經介紹并使用過了,這裡不再贅述。

(2)script exists

scripts exists sha1 [sha1 …]

此指令用于判斷sha1是否已經加載到redis記憶體中:

127.0.0.1:6379&gt; script exists

a5260dd66ce02462c5b5231c727b3f7772c0bcc5

傳回結果代表sha1 [sha1 …]被加載到redis記憶體的個數。

(3)script flush

script flush

此指令用于清除redis記憶體已經加載的所有lua腳本,在執行script flush後,a5260dd66ce02462c5b5231c727b3f7772c0bcc5不再存在:

127.0.0.1:6379&gt; script flush

1) (integer) 0

(4)script kill

script kill

此指令用于殺掉正在執行的lua腳本。如果lua腳本比較耗時,甚至lua腳本存在問題,那麼此時lua腳本的執行會阻塞redis,直到腳本執行完畢或者外部進行幹預将其結束。下面我們模拟一個lua腳本阻塞的情況進行說明。

下面的代碼會使lua進入死循環:

while 1 == 1

執行lua腳本,目前用戶端會阻塞:

127.0.0.1:6379&gt; eval 'while 1==1 do end'

redis提供了一個lua-time-limit參數,預設是5秒,它是lua腳本的“逾時時間”,但這個逾時時間僅僅是當lua腳本時間超過lua-time-limit後,向其他指令調用發送busy的信号,但是并不會停止掉服務端和用戶端的腳本執行,是以當達到lua-time-limit值之後,其他用戶端在執行正常的指令時,将會收到“busy redis is busy running a script”錯誤,并且提示使用script kill或者shutdown

nosave指令來殺掉這個busy的腳本:

127.0.0.1:6379&gt; get hello

(error) busy redis is busy running a

script. you can only call script kill or

shutdown nosave.

此時redis已經阻塞,無法處理正常的調用,這時可以選擇繼續等待,但更多時候需要快速将腳本殺掉。使用shutdown save顯然不太合适,是以選擇script kill,當script kill執行之後,用戶端調用會恢複:

127.0.0.1:6379&gt; script kill

但是有一點需要注意,如果目前lua腳本正在執行寫操作,那麼script kill将不會生效。例如,我們模拟一個不停的寫操作:

while 1==1

redis.call("set","k","v")

此時如果執行script kill,會收到如下異常資訊:

(error) unkillable sorry the script already

executed write commands against the

dataset. you can either wait the script termination or kill the server

in a

hard way using the shutdown nosave command.

上面提示lua腳本正在向redis執行寫指令,要麼等待腳本執行結束要麼使用shutdown save停掉redis服務。可見lua腳本雖然好用,但是使用不當破壞性也是難以想象的。