Redis Lua脚本中学教程(上)
失蹤人口回來啦!
有讀者問我為什么這么久都沒有出Redis Lua中學(xué)教程,表示村頭廁所已經(jīng)好久沒有紙了。其實我早就要寫這篇中學(xué)教程了,奈何最近太忙了,就一拖再拖,直到今天我終于又開始動筆了。忘記Lua相關(guān)概念的同學(xué)可以先回顧一下小學(xué)教程。
中學(xué)教程主要分為兩部分:Redis Lua的相關(guān)命令詳解和Lua的語法介紹。
前面我們簡單介紹了EVAL和EVALSHA命令。但是只有那點只是是沒辦法從中學(xué)畢業(yè)的,因此我們需要進(jìn)行更深入的學(xué)習(xí)。
EVAL
最早可用版本:2.6.0
用法:EVAL script numkeys key [key ...] arg [arg ...]
關(guān)于用法我們已經(jīng)演示過了,其中第一個參數(shù)是要執(zhí)行的Lua腳本,第二個參數(shù)是傳入腳本的參數(shù)個數(shù)。后面則是參數(shù)的key數(shù)組和value數(shù)組。
在Lua中執(zhí)行Redis命令的方法我們也介紹過,就是使用redis.call()和redis.pcall()兩個函數(shù)。它們之間唯一的不同就是當(dāng)Redis命令執(zhí)行錯誤時,redis.call()會拋出這個錯誤,使EVAL命令拋出錯誤,而redis.pcall()會捕獲這個錯誤,并返回Lua的錯誤表。
通常我們約定執(zhí)行命令的key都需要由參數(shù)傳入,命令必須在執(zhí)行之前進(jìn)行分析,以確定它作用于哪個key。這樣做的目的是為了在一定程度上保證EVAL執(zhí)行的Lua腳本的正確性。
Lua和Redis之間數(shù)據(jù)類型的轉(zhuǎn)換
在Redis執(zhí)行EVAL命令時,如果腳本中有call()或者pcall()命令,就會涉及到Redis和Lua之間數(shù)據(jù)類型轉(zhuǎn)換的問題。轉(zhuǎn)換規(guī)則要求,一個Redis的返回值轉(zhuǎn)換成Lua數(shù)據(jù)類型后,再轉(zhuǎn)換成Redis數(shù)據(jù)類型,其結(jié)果必須和初始值相同。所以每種類型是一一對應(yīng)的。轉(zhuǎn)換規(guī)則如下:
Redis與Lua互相轉(zhuǎn)換
| integer | number |
| bulk | string |
| multi bulk | table |
| status | table with a single ok field |
| error | table with a single err field |
| Nil bulk &Nil multi bulk | false boolean type |
除此之外,Lua到Redis的轉(zhuǎn)換還有一些其他的規(guī)則:
- Lua boolean true -> Redis integer reply with value of 1
- Lua只有一種數(shù)字類型,不會區(qū)分整數(shù)和浮點數(shù)。而數(shù)字類型只能轉(zhuǎn)換成Redis的integer類型,如果要返回浮點數(shù),那么在Lua中就需要返回一個字符串。
- Lua數(shù)組在轉(zhuǎn)換成Redis類型時,遇到nil就停止轉(zhuǎn)換
來個栗子驗證一下:
EVAL "return {1,2,3.3333,'foo',nil,'bar'}" 0 1) (integer) 1 2) (integer) 2 3) (integer) 3 4) "foo" 復(fù)制代碼可以看到bar沒有返回,并且3.333返回了3。
腳本的原子性
Redis運(yùn)行所有的Lua命令都使用相同的Lua解釋器。當(dāng)一個腳本正在執(zhí)行時,其他的腳本或Redis命令都不能執(zhí)行。這很像Redis的事務(wù)multi/exec。這意味著我們要盡量避免腳本的執(zhí)行時間過長。
腳本整體復(fù)制
當(dāng)腳本進(jìn)行傳播或者寫入AOF文件時,Redis通常會將腳本本身進(jìn)行傳播或?qū)懭階OF,而不是使用它產(chǎn)生的若干命令。原因很簡單,傳播整個腳本要比傳播一大堆生成的命令的速度要快。
從Redis3.2開始,可以只復(fù)制影響腳本執(zhí)行結(jié)果的語句,而不用復(fù)制整個腳本。這個復(fù)制整個腳本的方法有以下屬性:
- 如果輸入相同,腳本必須輸出相同的結(jié)果。即執(zhí)行結(jié)果不能依賴于隱式的變量,或依賴于I/O輸入
- Lua不會導(dǎo)出訪問系統(tǒng)時間或其他外部狀態(tài)的命令
- 如果先執(zhí)行了“隨機(jī)命令”(如RANDOMKEY,SRANDMEMBER,TIME),并改變了數(shù)據(jù)集,接著執(zhí)行腳本時會被阻塞。
- 在Redis4中,Lua腳本調(diào)用返回隨機(jī)順序的元素的命令時,會在返回之前進(jìn)行排序,也就是說,調(diào)用redis.call("smembers",KEYS[1]),每次返回的順序都相同。從Redis5開始就不需要排序了,因為Redis5復(fù)制的是產(chǎn)生影響的命令。
- Lua修改了偽隨機(jī)函數(shù)math.random和math.randomseed,使每次執(zhí)行腳本時seed都相同,而如果不執(zhí)行math.randomseed,只執(zhí)行math.random時,每次的結(jié)果也都相同。
復(fù)制命令隊列
在這種模式下,Redis在執(zhí)行腳本時會收集所有影響數(shù)據(jù)集的命令,當(dāng)腳本執(zhí)行完畢時,命令隊列會被放在事務(wù)中,發(fā)送給AOF文件。
Lua可以通過執(zhí)行redis.replicate_commands()函數(shù)來檢查復(fù)制模式,如果返回true表示當(dāng)前是復(fù)制命令模式,如果返回false,則是復(fù)制整個腳本模式。
可選擇的復(fù)制命令
腳本復(fù)制模式選擇好以后,就可以對復(fù)制到副本和AOF的方式進(jìn)行更多的控制。這是一種高級特性,因為濫用會切斷主從備份,和AOF持久化。如果我們只需要在master上執(zhí)行某些命令時,這一特性就變得很有用。例如我們需要計算一些中間值時,只需要在master上計算就好,那么這些命令就不必進(jìn)行復(fù)制。
從Redis3.2開始,有一個新的命令叫做redis.set_repl(),它可以用來控制復(fù)制方式,有如下選項(默認(rèn)是REPL_ALL):
redis.set_repl(redis.REPL_ALL) -- Replicate to AOF and replicas. redis.set_repl(redis.REPL_AOF) -- Replicate only to AOF. redis.set_repl(redis.REPL_REPLICA) -- Replicate only to replicas (Redis >= 5) redis.set_repl(redis.REPL_SLAVE) -- Used for backward compatibility, the same as REPL_REPLICA. redis.set_repl(redis.REPL_NONE) -- Don't replicate at all. 復(fù)制代碼全局變量
為了避免數(shù)據(jù)泄露,Redis腳本不允許創(chuàng)建全局變量。如果必須有一個公共變量,可以使用Redis的key來代替。在EVAL命令中創(chuàng)建一個全局變量會引起一個異常。
> eval 'a=10' 0 (error) ERR Error running script (call to f_933044db579a2f8fd45d8065f04a8d0249383e57): user_script:1: Script attempted to create global variable 'a 復(fù)制代碼關(guān)于SELECT的使用
在Lua腳本中使用SELECT就像在正常客戶端中使用一樣。值得一提的是,在Redis2.8.12之前,Lua腳本中執(zhí)行SELECT是會影響到客戶端的,而從2.8.12開始,Lua腳本中的SELECT只會在腳本執(zhí)行過程中生效。這點在Redis版本升級時需要注意,因為升級前后,命令的語義會改變。
可用的庫
Lua腳本中有許多庫,但并不是都能在Redis中使用,其中可以使用的有:
- base lib.
- table lib.
- string lib.
- math lib.
- struct lib.
- cjson lib.
- cmsgpack lib.
- bitop lib.
- redis.sha1hex function.
- redis.breakpoint and redis.debug function in the context of the Redis Lua debugger.
struct, CJSON and cmsgpack是外部庫,其他的都是Lua的標(biāo)準(zhǔn)庫。
在腳本中打印Redis日志
使用redis.log(loglevel,message)函數(shù)可以在Lua腳本中打印Redis日志。
loglevel包括:
- redis.LOG_DEBUG
- redis.LOG_VERBOSE
- redis.LOG_NOTICE
- redis.LOG_WARNING
它們與Redis的日志等級是對應(yīng)的。
沙箱和最大執(zhí)行時間
腳本不應(yīng)該訪問外部系統(tǒng),包括文件系統(tǒng)和其他系統(tǒng)。腳本應(yīng)該只能操作Redis數(shù)據(jù)和傳入進(jìn)來的參數(shù)。
腳本默認(rèn)的最大執(zhí)行時間是5秒(正常腳本執(zhí)行時間都是毫秒級,所以5秒已經(jīng)足夠長了)。可以通過修改lua-time-limit變量來控制最大執(zhí)行時間。
當(dāng)腳本執(zhí)行時間超過最大執(zhí)行時間時,并不會被自動終止,因為這違反了腳本的原子性原則。當(dāng)一個腳本執(zhí)行時間過長時,Redis會有如下操作:
- Redis記錄下這個腳本執(zhí)行時間過長
- 其他客戶端開始接收命令,但是所有的命令都會會返回繁忙,除了SCRIPT KILL 和 SHUTDOWN NOSAVE
- 如果一個腳本僅執(zhí)行只讀命令,則可以用SCRIPT KILL命令來停止它。
- 如果腳本執(zhí)行了寫入命令,那么只能用SHUTDOWN NOSAVE來終止服務(wù)器,當(dāng)前的所有數(shù)據(jù)都不會保存到磁盤。
EVALSHA
最早可用版本:2.6.0
用法:EVALSHA sha1 numkeys key [key ...] arg [arg ...]
該命令用來執(zhí)行緩存在服務(wù)器上的腳本,sha1為腳本的唯一標(biāo)識。
使用EVAL命令必須每次都要把腳本從客戶端傳到服務(wù)器,由于Redis的內(nèi)部緩存機(jī)制,它并不會每次都重新編譯腳本,但是傳輸上仍然浪費(fèi)帶寬。
另一方面,如果使用特殊命令或者通過redis.conf來定義命令會有以下問題:
- 不同實例有不同的實現(xiàn)方式
- 發(fā)布將會很困難,特別是分布式環(huán)境,因為要保證所有實例都包含給定的命令
- 讀應(yīng)用程序代碼時,由于它調(diào)用了服務(wù)端命令,會不清楚代碼的語義
為了避免這些問題,同時避免浪費(fèi)帶寬,Redis實現(xiàn)了EVALSHA命令。
如果服務(wù)器中沒有緩存指定的腳本,會返回給客戶端腳本不存在的錯誤信息。
SCRIPT DEBUG
最早可用版本:3.2.0
時間復(fù)雜度:O(1)
用法:SCRIPT DEBUG YES|SYNC|NO
該命令用于設(shè)置隨后執(zhí)行的EVAL命令的調(diào)試模式。Redis包含一個完整的Lua調(diào)試器,代號為LDB,可以使編寫復(fù)雜腳本的任務(wù)更加簡單,在調(diào)試模式下,Redis充當(dāng)遠(yuǎn)程調(diào)試服務(wù)器,客戶端可以逐步執(zhí)行腳本,設(shè)置斷點,檢查變量等。想了解更多調(diào)試器內(nèi)容的可以查看官方文檔Redis Lua debugger。
LDB可以設(shè)置成異步或同步模式。異步模式下,服務(wù)器會fork出一個調(diào)試會話,不會阻塞主會話,,調(diào)試會話結(jié)束后,所有數(shù)據(jù)都會回滾。同步模式則會阻塞會話,并保留調(diào)試過程中數(shù)據(jù)的改變。
SCRIPT EXISTS
最早可用版本:2.6.0
時間復(fù)雜度:O(N),N是腳本數(shù)量
返回腳本是否存在于緩存中(存在返回1,不存在返回0)。這個命令適合在管道前執(zhí)行,以保證管道中的所有腳本都已經(jīng)加載到服務(wù)器端了,如果沒有,需要用SCRIPT LOAD命令進(jìn)行加載。
SCRIPT FLUSH
最早可用版本:2.6.0
時間復(fù)雜度:O(N),N是緩存中的腳本數(shù)
刷新緩存中的腳本,這一命令常在云服務(wù)上被使用。
SCRIPT KILL
最早可用版本:2.6.0
時間復(fù)雜度:O(1)
停止當(dāng)前正在執(zhí)行的Lua腳本,通常用來停止執(zhí)行時間過長的腳本。停止后,被阻塞的客戶端會拋出一個錯誤。
SCRIPT LOAD
最早可用版本:2.6.0
時間復(fù)雜度:O(N),N是腳本的字節(jié)數(shù)
該命令用于將腳本加載到服務(wù)器端的緩存中,但不會執(zhí)行。加載后,服務(wù)器會一直緩存,因為良好的應(yīng)用程序不太可能有太多不同的腳本導(dǎo)致內(nèi)存不足。每個腳本都像一個新命令的緩存,所以即使是大型應(yīng)用程序,也就有幾百個,它們占用的內(nèi)存是微不足道的。
小結(jié)
本文介紹了Redis Lua相關(guān)的命令。其中EVAL和EVALSHA用來執(zhí)行腳本。腳本執(zhí)行具有原子性。腳本的復(fù)制和傳播可以根據(jù)需要設(shè)置。腳本中不能定義全局變量。
轉(zhuǎn)載于:https://juejin.im/post/5d079f746fb9a07ee30e19f1
總結(jié)
以上是生活随笔為你收集整理的Redis Lua脚本中学教程(上)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 开发者必备Docker命令
- 下一篇: 看到腾讯反驳360的文章,笑死我了。