Redis提供了很是豐富的指令集,可是用戶依然不知足,但願能夠自定義擴充若干指令來完成一些特定領域的問題。Redis 爲這樣的用戶場景提供了 lua腳本支持,用戶能夠向服務器發送 lua 腳原本執行自定義動做,獲取腳本的響應數據。Redis 服務器會單線程原子性執行 lua 腳本,保證 lua 腳本在處理的過程當中不會被任意其它請求打斷。web
圖片redis
好比在《Redis 深度歷險》分佈式鎖小節,咱們提到了 del_if_equals 僞指令,它能夠將匹配 key 和刪除 key 合併在一塊兒原子性執行,Redis原生沒有提供這樣功能的指令,它可使用 lua腳原本完成。算法
那上面這個腳本如何執行呢?使用 EVAL 指令數組
EVAL 指令的第一個參數是腳本內容字符串,上面的例子咱們將 lua 腳本壓縮成一行以單引號圍起來是爲了方便命令行執行。而後是 key 的數量以及每一個 key 串,最後是一系列附加參數字符串。附加參數的數量不須要和key保持一致,能夠徹底沒有附加參數。緩存
上面的例子中只有 1 個 key,它就是 foo,緊接着 bar 是惟一的附加參數。在 lua 腳本中,數組下標是從 1 開始,因此經過 KEYS[1] 就能夠獲得 第一個 key,經過 ARGV[1] 就能夠獲得第一個附加參數。redis.call 函數可讓咱們調用 Redis 的原生指令,上面的代碼分別調用了 get 指令和 del 指令。return 返回的結果將會返回給客戶端。服務器
SCRIPT LOAD 和 EVALSHA 指令
在上面的例子中,腳本的內容很短。若是腳本的內容很長,並且客戶端須要頻繁執行,那麼每次都須要傳遞冗長的腳本內容勢必比較浪費網絡流量。因此 Redis 還提供了 SCRIPT LOAD 和 EVALSHA 指令來解決這個問題。網絡
SCRIPT LOAD 指令用於將客戶端提供的 lua 腳本傳遞到服務器而不執行,可是會獲得腳本的惟一 ID,這個惟一 ID 是用來惟一標識服務器緩存的這段 lua 腳本,它是由 Redis 使用 sha1 算法揉捏腳本內容而獲得的一個很長的字符串。有了這個惟一 ID,後面客戶端就能夠經過 EVALSHA 指令反覆執行這個腳本了。分佈式
咱們知道 Redis 有 incrby 指令能夠完成整數的自增操做,可是沒有提供自乘這樣的指令。函數
下面咱們使用 SCRIPT LOAD 和 EVALSHA 指令來完成自乘運算。lua
先將上面的腳本單行化,語句之間使用分號隔開
加載腳本
命令行輸出了很長的字符串,它就是腳本的惟一標識,下面咱們使用這個惟一標識來執行指令
錯誤處理
上面的腳本參數要求傳入的附加參數必須是整數,若是沒有傳遞整數會怎樣呢?
能夠看到客戶端輸出了服務器返回的通用錯誤消息,注意這是一個動態拋出的異常,Redis 會保護主線程不會由於腳本的錯誤而致使服務器崩潰,近似於在腳本的外圍有一個很大的 try catch語句包裹。在 lua 腳本執行的過程當中遇到了錯誤,同 redis 的事務同樣,那些經過 redis.call 函數已經執行過的指令對服務器狀態產生影響是沒法撤銷的,在編寫 lua 代碼時必定要當心,避免沒有考慮到的判斷條件致使腳本沒有徹底執行。
若是讀者對 lua 語言有所瞭解就知道 lua 原生沒有提供 try catch 語句,那上面提到的異常包裹語句到底是用什麼來實現的呢?lua 的替代方案是內置了 pcall(f) 函數調用。pcall 的意思是 protected call,它會讓 f 函數運行在保護模式下,f 若是出現了錯誤,pcall 調用會返回 false 和錯誤信息。而普通的 call(f) 調用在遇到錯誤時只會向上拋出異常。在 Redis 的源碼中能夠看到 lua 腳本的執行被包裹在 pcall 函數調用中。
Redis 在 lua 腳本中除了提供了 redis.call 函數外,一樣也提供了redis.pcall函數。前者遇到錯誤向上拋出異常,後者會返回錯誤信息。使用時必定要注意 call 函數出錯時會中斷腳本的執行,爲了保證腳本的原子性,要謹慎使用。
錯誤傳遞
redis.call 函數調用會產生錯誤,腳本遇到這種錯誤會返回怎樣的信息呢?咱們再看個例子
客戶端輸出的依然是一個通用的錯誤消息,而不是 incr 調用本應該返回的 WRONGTYPE 類型的錯誤消息。Redis 內部在處理 redis.call 遇到錯誤時是向上拋出異常,外圍的用戶看不見的 pcall調用捕獲到腳本異常時會向客戶端回覆通用的錯誤信息。若是咱們將上面的 call 改爲 pcall,結果就會不同,它能夠將內部指令返回的特定錯誤向上傳遞。
腳本死循環怎麼辦?
Redis 的指令執行是個單線程,這個單線程還要執行來自客戶端的 lua 腳本。若是 lua 腳本中來一個死循環,是否是 Redis 就完蛋了?Redis 爲了解決這個問題,它提供了 script kill 指令用於動態殺死一個執行時間超時的 lua 腳本。不過 script kill 的執行有一個重要的前提,那就是當前正在執行的腳本沒有對 Redis 的內部數據狀態進行修改,由於 Redis 不容許 script kill 破壞腳本執行的原子性。好比腳本內部使用了 redis.call("set", key, value) 修改了內部的數據,那麼 script kill 執行時服務器會返回錯誤。下面咱們來嘗試如下 script kill指令。
eval指令執行後,能夠明顯看出來 redis 卡死了,死活沒有任何響應,若是去觀察 Redis 服務器日誌能夠看到日誌在瘋狂輸出 hello 字符串。這時候就必須從新開啓一個 redis-cli 來執行 script kill 指令。
再回過頭看 eval 指令的輸出
看到這裏細心的同窗會注意到兩個疑點,第一個是 script kill 指令爲何執行了 2.58 秒,第二個是腳本都卡死了,Redis 哪裏來的閒功夫接受 script kill 指令。若是你本身嘗試了在第二個窗口執行 redis-cli 去鏈接服務器,你還會發現第三個疑點,redis-cli 創建鏈接有點慢,大約頓了有 1 秒左右。
Script Kill 的原理
下面我就要開始揭祕kill的原理了,lua 腳本引擎功能太強大了,它提供了各式各樣的鉤子函數,它容許在內部虛擬機執行指令時運行鉤子代碼。好比每執行 N 條指令執行一次某個鉤子函數,Redis 正是使用了這個鉤子函數。
Redis 在鉤子函數裏會忙裏偷閒去處理客戶端的請求,而且只有在發現 lua 腳本執行超時以後纔會去處理請求,這個超時時間默認是 5 秒。因而上面提出的三個疑點也就煙消雲散了。
思考題
在延時隊列小節,咱們使用 zrangebyscore 和 zdel 兩條指令來爭搶延時隊列中的任務,經過 zdel 的返回值來決定是哪一個客戶端搶到了任務,這意味着那些沒有搶到任務的客戶端會有這樣一種感覺 —— 到了嘴邊的肉(任務)最後還被別人搶走了,會很不爽。若是可使用 lua 腳原本實現爭搶邏輯,將 zrangebyscore 和zdel指令原子性執行就不會存在這種問題,讀者能夠嘗試一下