【Redis】Redis中使用Lua脚本
Lua是一種輕量小巧的腳本語言,用標(biāo)準(zhǔn)C語言編寫并以源代碼形式開放,其設(shè)計(jì)目的是為了嵌入應(yīng)用程序中,從而為應(yīng)用程序提供靈活的擴(kuò)展和定制功能。
Lua具體語法參考:https://www.runoob.com/lua/lua-tutorial.html
腳本的原子性
Redis使用單個(gè)Lua解釋器去運(yùn)行所有腳本,并且Redis也保證腳本會(huì)以原子性(atomic)的方式執(zhí)行:當(dāng)某個(gè)腳本正在運(yùn)行的時(shí)候,不會(huì)有其他腳本或 Redis命令被執(zhí)行。這和使用MULTI/EXEC包圍的事務(wù)很類似。
在其他別的客戶端看來,腳本的效果要么是不可見的,要么就是已完成的。
另一方面,這也意味著,執(zhí)行一個(gè)運(yùn)行緩慢的腳本并不是一個(gè)好主意。寫一個(gè)跑得很快很順溜的腳本并不難,因?yàn)槟_本的運(yùn)行開銷非常少,但是當(dāng)你不得不使用一些跑得比較慢的腳本時(shí),請(qǐng)小心,因?yàn)楫?dāng)這些蝸牛腳本在慢吞吞地運(yùn)行的時(shí)候,其他客戶端會(huì)因?yàn)榉?wù)器正忙而無法執(zhí)行命令。
eval命令的使用
eval和evalsha命令是從Redis2.6.0版本開始引入的,使用內(nèi)置的Lua解釋器,可以對(duì)Lua腳本進(jìn)行求值。
eval命令的說明:
> help evalEVAL script numkeys key [key ...] arg [arg ...]summary: Execute a Lua script server sidesince: 2.6.0group: scripting參數(shù)說明:
- script:一段Lua腳本程序,這段Lua腳本不需要也不應(yīng)該定義函數(shù),它運(yùn)行在Redis服務(wù)器中。
- numkeys:鍵名參數(shù)的個(gè)數(shù)。
- key[]: 鍵名參數(shù),表示在腳本中所用到的那些Redis鍵(key),這些鍵名參數(shù)可以在Lua中通過全局變量KEYS數(shù)組,用1為基址的形式訪問(KEYS[1]、KEYS[2],以此類推)。
- arg[]:不是鍵名參數(shù)的附加參數(shù),可以在Lua中通過全局變量ARGV數(shù)組訪問,訪問的形式和KEYS變量類似(ARGV[1]、ARGV[2],諸如此類)。
舉例說明:
> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 a b c d 1) "a" 2) "b" 3) "c" 4) "d"返回結(jié)果是Redis multi bulk replies的Lua數(shù)組,這是一個(gè)Redis的返回類型,其他客戶端庫(如JAVA客戶端)可能會(huì)將他們轉(zhuǎn)換成數(shù)組類型。
Lua中執(zhí)行redis命令
在Lua中,可以通過內(nèi)置的函數(shù)redis.call()和redis.pcall()來執(zhí)行redis命令。
redis.call()和redis.pcall()兩個(gè)函數(shù)的參數(shù)可以是任意的Redis命令:
> eval "return redis.call('set','foo','bar')" 0 OK需要注意的是,上面這段腳本的確實(shí)現(xiàn)了將鍵foo的值設(shè)為bar的目的,但是,它違反了EVAL命令的語義,因?yàn)槟_本里使用的所有鍵都應(yīng)該由KEYS數(shù)組來傳遞,就像這樣:
> eval "return redis.call('set',KEYS[1],'bar')" 1 foo OK要求使用正確的形式來傳遞鍵(key)是有原因的,因?yàn)椴粌H僅是EVAL這個(gè)命令,所有的Redis命令,在執(zhí)行之前都會(huì)被分析,借此來確定命令會(huì)對(duì)哪些鍵進(jìn)行操作。
因此,對(duì)于EVAL命令來說,必須使用正確的形式來傳遞鍵,才能確保分析工作正確地執(zhí)行。除此之外,使用正確的形式來傳遞鍵還有很多其他好處,它的一個(gè)特別重要的用途就是確保Redis集群可以將你的請(qǐng)求發(fā)送到正確的集群節(jié)點(diǎn)。
redis.call()與redis.pcall()很類似,他們唯一的區(qū)別是當(dāng)redis命令執(zhí)行結(jié)果返回錯(cuò)誤時(shí),redis.call()將返回給調(diào)用者一個(gè)錯(cuò)誤,而redis.pcall()會(huì)將捕獲的錯(cuò)誤以Lua表的形式返回。
下面的例子演示了redis.call()與redis.pcall()的區(qū)別:
> eval "return redis.call('set1',KEYS[1],'bar')" 1 foo (error) ERR Error running script (call to f_d968406ee98123006fa91fd2ee764d4f7f859dd7): @user_script:1: @user_script: 1: Unknown Redis command called from Lua script> eval "return redis.pcall('set1',KEYS[1],'bar')" 1 foo (error) @user_script: 1: Unknown Redis command called from Lua script> eval "return type(redis.call('set1',KEYS[1],'bar'))" 1 foo (error) ERR Error running script (call to f_c62b83c8313fd8f2557865e37d2bb5133f1789af): @user_script:1: @user_script: 1: Unknown Redis command called from Lua script> eval "return type(redis.pcall('set1',KEYS[1],'bar'))" 1 foo "table"Lua數(shù)據(jù)類型和Redis數(shù)據(jù)類型之間轉(zhuǎn)換
當(dāng)Lua通過call()或pcall()函數(shù)執(zhí)行Redis命令的時(shí)候,命令的返回值會(huì)被轉(zhuǎn)換成Lua數(shù)據(jù)結(jié)構(gòu)。
同樣地,當(dāng)Lua腳本在Redis內(nèi)置的解釋器里運(yùn)行時(shí),Lua腳本的返回值也會(huì)被轉(zhuǎn)換成Redis協(xié)議(protocol),然后由EVAL將值返回給客戶端。
數(shù)據(jù)類型之間的轉(zhuǎn)換遵循這樣一個(gè)設(shè)計(jì)原則:如果將一個(gè)Redis值轉(zhuǎn)換成Lua值,之后再將轉(zhuǎn)換所得的Lua值轉(zhuǎn)換回Redis值,那么這個(gè)轉(zhuǎn)換所得的Redis 值應(yīng)該和最初時(shí)的Redis值一樣。
換句話說,Lua類型和Redis類型之間存在著一一對(duì)應(yīng)的轉(zhuǎn)換關(guān)系。
| Redis integer reply | Lua number |
| Redis bulk reply | Lua string |
| Redis multi bulk reply | Lua table (may have other Redis data types nested) |
| Redis status reply | Lua table with a single ok field containing the status |
| Redis error reply | Lua table with a single err field containing the error |
| Redis Nil bulk reply and Nil multi bulk reply | Lua false boolean type |
從Lua轉(zhuǎn)換到Redis有一條額外的規(guī)則,這條規(guī)則沒有和它對(duì)應(yīng)的從Redis轉(zhuǎn)換到Lua的規(guī)則:
- Lua boolean true -> Redis integer reply with value of 1. / Lua 布爾值 true 轉(zhuǎn)換成 Redis 整數(shù)回復(fù)中的 1
Lua中整數(shù)和浮點(diǎn)數(shù)之間沒有什么區(qū)別。因此,我們始終將Lua的數(shù)字轉(zhuǎn)換成整數(shù)的回復(fù),這樣將舍去小數(shù)部分。如果你想從Lua返回一個(gè)浮點(diǎn)數(shù),你應(yīng)該將它作為一個(gè)字符串,比如ZSCORE命令。
以下是幾個(gè)類型轉(zhuǎn)換的例子:
> eval "return 10" 0 (integer) 10> eval "return {1,2,{3,'Hello World!'}}" 0 1) (integer) 1 2) (integer) 2 3) 1) (integer) 32) "Hello World!"> eval "return redis.call('get','foo')" 0 "bar"最后一個(gè)例子展示如果是Lua直接命令調(diào)用它是如何可以從redis.call()或redis.pcall()接收到準(zhǔn)確的返回值。
下面的例子我們可以看到浮點(diǎn)數(shù)和nil將怎么樣處理:
> eval "return {1,2,3.3333,'foo',nil,'bar'}" 0 1) (integer) 1 2) (integer) 2 3) (integer) 3 4) "foo"正如你看到的3.333被轉(zhuǎn)換成了3,并且nil后面的字符串bar沒有被返回回來。
可以使用tostring()函數(shù)將數(shù)字轉(zhuǎn)字符串:
> eval "return tostring(3.3333)" 0 "3.3333"有兩個(gè)輔助函數(shù)從Lua返回Redis的類型:
- redis.error_reply(error_string):returns an error reply. This function simply returns the single field table with the err field set to the specified string for you.
- redis.status_reply(status_string):returns a status reply. This function simply returns the single field table with the ok field set to the specified string for you.
使用redis.error_reply()函數(shù)與直接返回一個(gè)table效果一樣:
> eval "return {err='My Error'}" 0 (error) My Error> eval "return redis.error_reply('My Error')" 0 (error) My ErrorEVALSHA
EVAL命令要求你在每次執(zhí)行腳本的時(shí)候都發(fā)送一次腳本主體(script body)。Redis有一個(gè)內(nèi)部的緩存機(jī)制,因此它不會(huì)每次都重新編譯腳本,不過在很多場(chǎng)合,付出無謂的帶寬來傳送腳本主體并不是最佳選擇。
為了減少帶寬的消耗,Redis實(shí)現(xiàn)了EVALSHA命令,它的作用和EVAL一樣,都用于對(duì)腳本求值,但它接受的第一個(gè)參數(shù)不是腳本,而是腳本的SHA1校驗(yàn)和(sum)。
如果服務(wù)器還記得給定的SHA1校驗(yàn)和所指定的腳本,那么執(zhí)行這個(gè)腳本,如果服務(wù)器不記得給定的SHA1校驗(yàn)和所指定的腳本,那么它返回一個(gè)特殊的錯(cuò)誤,提醒用戶使用EVAL代替EVALSHA。
以下是示例:
> set foo bar OK> eval "return redis.call('get','foo')" 0 "bar"> evalsha 6b1bf486c81ceb7edf3c093f4c48582e38c0e791 0 "bar"> evalsha ffffffffffffffffffffffffffffffffffffffff 0 (error) NOSCRIPT No matching script. Please use EVAL.客戶端庫的底層實(shí)現(xiàn)可以一直樂觀地使用EVALSHA來代替EVAL,并期望著要使用的腳本已經(jīng)保存在服務(wù)器上了,只有當(dāng)NOSCRIPT錯(cuò)誤發(fā)生時(shí),才使用 EVAL命令重新發(fā)送腳本,這樣就可以最大限度地節(jié)省帶寬。
這也說明了執(zhí)行EVAL命令時(shí),使用正確的格式來傳遞鍵名參數(shù)和附加參數(shù)的重要性:因?yàn)槿绻麑?shù)硬寫在腳本中,那么每次當(dāng)參數(shù)改變的時(shí)候,都要重新發(fā)送腳本,即使腳本的主體并沒有改變,相反,通過使用正確的格式來傳遞鍵名參數(shù)和附加參數(shù),就可以在腳本主體不變的情況下,直接使用EVALSHA 命令對(duì)腳本進(jìn)行復(fù)用,免去了無謂的帶寬消耗。
腳本緩存
Redis保證所有被運(yùn)行過的腳本都會(huì)被永久保存在腳本緩存當(dāng)中,這意味著,當(dāng)EVAL命令在一個(gè)Redis實(shí)例上成功執(zhí)行某個(gè)腳本之后,隨后針對(duì)這個(gè)腳本的所有EVALSHA命令都會(huì)成功執(zhí)行。
刷新腳本緩存的唯一辦法是顯式地調(diào)用SCRIPT FLUSH命令,這個(gè)命令會(huì)清空運(yùn)行過的所有腳本的緩存。通常只有在云計(jì)算環(huán)境中,Redis實(shí)例被改作其他客戶或者別的應(yīng)用程序的實(shí)例時(shí),才會(huì)執(zhí)行這個(gè)命令。
緩存可以長時(shí)間儲(chǔ)存而不產(chǎn)生內(nèi)存問題的原因是,它們的體積非常小,而且數(shù)量也非常少,即使腳本在概念上類似于實(shí)現(xiàn)一個(gè)新命令,即使在一個(gè)大規(guī)模的程序里有成百上千的腳本,即使這些腳本會(huì)經(jīng)常修改,即便如此,儲(chǔ)存這些腳本的內(nèi)存仍然是微不足道的。
事實(shí)上,用戶會(huì)發(fā)現(xiàn)Redis不移除緩存中的腳本實(shí)際上是一個(gè)好主意。比如說,對(duì)于一個(gè)和Redis保持持久化鏈接(persistent connection)的程序來說,它可以確信,執(zhí)行過一次的腳本會(huì)一直保留在內(nèi)存當(dāng)中,因此它可以在流水線中使用EVALSHA命令而不必?fù)?dān)心因?yàn)檎也坏剿璧哪_本而產(chǎn)生錯(cuò)誤。
Redis提供了以下幾個(gè)SCRIPT命令,用于對(duì)腳本子系統(tǒng)(scripting subsystem)進(jìn)行控制:
- SCRIPT FLUSH:清除所有腳本緩存
- SCRIPT EXISTS:根據(jù)給定的腳本校驗(yàn)和,檢查指定的腳本是否存在于腳本緩存
- SCRIPT LOAD:將一個(gè)腳本裝入腳本緩存,但并不立即運(yùn)行它
- SCRIPT KILL:殺死當(dāng)前正在運(yùn)行的腳本
可用庫
Redis Lua解釋器可用加載以下Lua庫:
- base lib.
- table lib.
- string lib.
- math lib.
- debug lib.
- struct lib.
- cjson lib.
- cmsgpack lib.
- bitop lib.
- redis.sha1hex function.
每一個(gè)Redis實(shí)例都擁有以上的所有類庫,以確保您使用腳本的環(huán)境都是一樣的。
struct,CJSON和cmsgpack都是外部庫,所有其他庫都是標(biāo)準(zhǔn)Lua庫。
CJSON庫為Lua提供極快的JSON處理:
> eval 'return cjson.encode({["foo"]= "bar"})' 0 "{\"foo\":\"bar\"}"> eval 'return cjson.decode(ARGV[1])["foo"]' 0 "{\"foo\":\"bar\"}" "bar"> eval "local table = {} table['foo']='bar' table['hello']='world' return cjson.encode(table)" 0 "{\"hello\":\"world\",\"foo\":\"bar\"}"沙箱(sandbox)和最大執(zhí)行時(shí)間
腳本應(yīng)該僅僅用于傳遞參數(shù)和對(duì)Redis數(shù)據(jù)進(jìn)行處理,它不應(yīng)該嘗試去訪問外部系統(tǒng)(比如文件系統(tǒng)),或者執(zhí)行任何系統(tǒng)調(diào)用。
除此之外,腳本還有一個(gè)最大執(zhí)行時(shí)間限制,它的默認(rèn)值是5秒鐘,一般正常運(yùn)作的腳本通常可以在幾分之幾毫秒之內(nèi)完成,花不了那么多時(shí)間,這個(gè)限制主要是為了防止因編程錯(cuò)誤而造成的無限循環(huán)而設(shè)置的。
最大執(zhí)行時(shí)間的長短由lua-time-limit選項(xiàng)來控制(以毫秒為單位),可以通過編輯redis.conf文件或者使用CONFIG GET和CONFIG SET命令來修改它。
當(dāng)一個(gè)腳本達(dá)到最大執(zhí)行時(shí)間的時(shí)候,它并不會(huì)自動(dòng)被Redis結(jié)束,因?yàn)镽edis必須保證腳本執(zhí)行的原子性,而中途停止腳本的運(yùn)行意味著可能會(huì)留下未處理完的數(shù)據(jù)在數(shù)據(jù)集(data set)里面。
因此,當(dāng)腳本運(yùn)行的時(shí)間超過最大執(zhí)行時(shí)間后,以下動(dòng)作會(huì)被執(zhí)行:
- Redis記錄一個(gè)腳本正在超時(shí)運(yùn)行
- Redis開始重新接受其他客戶端的命令請(qǐng)求,但是只有SCRIPT KILL和SHUTDOWN NOSAVE兩個(gè)命令會(huì)被處理,對(duì)于其他命令請(qǐng)求,Redis服務(wù)器只是簡單地返回BUSY錯(cuò)誤。
- 可以使用SCRIPT KILL命令將一個(gè)僅執(zhí)行只讀命令的腳本殺死,因?yàn)橹蛔x命令并不修改數(shù)據(jù),因此殺死這個(gè)腳本并不破壞數(shù)據(jù)的完整性
- 如果腳本已經(jīng)執(zhí)行過寫命令,那么唯一允許執(zhí)行的操作就是SHUTDOWN NOSAVE,它通過停止服務(wù)器來阻止當(dāng)前數(shù)據(jù)集寫入磁盤
流水線(pipeline)上下文(context)中的EVALSHA
在流水線請(qǐng)求的上下文中使用EVALSHA命令時(shí),要特別小心,因?yàn)樵诹魉€中,必須保證命令的執(zhí)行順序。
一旦在流水線中因?yàn)镋VALSHA命令而發(fā)生NOSCRIPT錯(cuò)誤,那么這個(gè)流水線就再也沒有辦法重新執(zhí)行了,否則的話,命令的執(zhí)行順序就會(huì)被打亂。
為了防止出現(xiàn)以上所說的問題,客戶端庫實(shí)現(xiàn)應(yīng)該實(shí)施以下的其中一項(xiàng)措施:
- 總是在流水線中使用EVAL命令
- 檢查流水線中要用到的所有命令,找到其中的EVAL命令,并使用SCRIPT EXISTS命令檢查要用到的腳本是不是全都已經(jīng)保存在緩存里面了。如果所需的全部腳本都可以在緩存里找到,那么就可以放心地將所有EVAL命令改成EVALSHA命令,否則的話,就要在流水線的頂端(top)將缺少的腳本用SCRIPT LOAD 命令加上去。
總結(jié)
以上是生活随笔為你收集整理的【Redis】Redis中使用Lua脚本的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: [react] React中验证prop
- 下一篇: 初学Vue.js,从头来过~