<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> multi
ok
127.0.0.1:6379> sadd user:a:follow
user:b
queued
127.0.0.1:6379> sadd user:b:fans user:a
可以看到sadd指令此時的傳回結果是queued,代表指令并沒有真正執行,而是暫時儲存在redis中。如果此時另一個用戶端執行sismember user:a:follow user:b傳回結果應該為0。
127.0.0.1:6379> sismember user:a:follow
(integer) 0
隻有當exec執行後,使用者a關注使用者b的行為才算完成,如下所示傳回的兩個結果對應sadd指令。
127.0.0.1:6379> exec
1) (integer) 1
2) (integer) 1
(integer) 1
如果要停止事務的執行,可以使用discard指令代替exec指令即可。
127.0.0.1:6379> discard
如果事務中的指令出現錯誤,redis的處理機制也不盡相同。
1??指令錯誤
例如下面操作錯将set寫成了sett,屬于文法錯誤,會造成整個事務無法執行,key和counter的值未發生變化:
127.0.0.1:6388> mget key counter
1) "hello"
2) "100"
127.0.0.1:6388> multi
127.0.0.1:6388> sett key world
(error) err unknown command 'sett'
127.0.0.1:6388> incr counter
127.0.0.1:6388> exec
(error) execabort transaction discarded
because of previous errors.
2.?運作時錯誤
例如使用者b在添加粉絲清單時,誤把sadd指令寫成了zadd指令,這種就是運作時指令,因為文法是正确的:
127.0.0.1:6379> 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> set key "java"
#t2:用戶端1
127.0.0.1:6379> watch key
#t3:用戶端1
#t4:用戶端2
127.0.0.1:6379> append key python
(integer) 11
#t5:用戶端1
127.0.0.1:6379> append key jedis
#t6:用戶端1
(nil)
#t7:用戶端1
127.0.0.1:6379> 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 <= 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> 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> 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> 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> 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> 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> script exists
a5260dd66ce02462c5b5231c727b3f7772c0bcc5
傳回結果代表sha1 [sha1 …]被加載到redis記憶體的個數。
(3)script flush
script flush
此指令用于清除redis記憶體已經加載的所有lua腳本,在執行script flush後,a5260dd66ce02462c5b5231c727b3f7772c0bcc5不再存在:
127.0.0.1:6379> 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> 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> 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> 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腳本雖然好用,但是使用不當破壞性也是難以想象的。