Redis Lua腳本中學教程(上)

失蹤人口回來啦!git

有讀者問我爲何這麼久都沒有出Redis Lua中學教程,表示村頭廁所已經很久沒有紙了。其實我早就要寫這篇中學教程了,奈何最近太忙了,就一拖再拖,直到今天我終於又開始動筆了。忘記Lua相關概念的同窗能夠先回顧一下小學教程github

中學教程主要分爲兩部分:Redis Lua的相關命令詳解和Lua的語法介紹。redis

前面咱們簡單介紹了EVAL和EVALSHA命令。可是隻有那點只是是沒辦法從中學畢業的,所以咱們須要進行更深刻的學習。json

EVAL

最先可用版本:2.6.0數組

用法:EVAL script numkeys key [key ...] arg [arg ...]緩存

關於用法咱們已經演示過了,其中第一個參數是要執行的Lua腳本,第二個參數是傳入腳本的參數個數。後面則是參數的key數組和value數組。bash

在Lua中執行Redis命令的方法咱們也介紹過,就是使用redis.call()和redis.pcall()兩個函數。它們之間惟一的不一樣就是當Redis命令執行錯誤時,redis.call()會拋出這個錯誤,使EVAL命令拋出錯誤,而redis.pcall()會捕獲這個錯誤,並返回Lua的錯誤表。服務器

一般咱們約定執行命令的key都須要由參數傳入,命令必須在執行以前進行分析,以肯定它做用於哪一個key。這樣作的目的是爲了在必定程度上保證EVAL執行的Lua腳本的正確性。dom

Lua和Redis之間數據類型的轉換

在Redis執行EVAL命令時,若是腳本中有call()或者pcall()命令,就會涉及到Redis和Lua之間數據類型轉換的問題。轉換規則要求,一個Redis的返回值轉換成Lua數據類型後,再轉換成Redis數據類型,其結果必須和初始值相同。因此每種類型是一一對應的。轉換規則以下:異步

Redis與Lua互相轉換
Redis Lua
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的轉換還有一些其餘的規則:

  • Lua boolean true -> Redis integer reply with value of 1
  • Lua只有一種數字類型,不會區分整數和浮點數。而數字類型只能轉換成Redis的integer類型,若是要返回浮點數,那麼在Lua中就須要返回一個字符串。
  • Lua數組在轉換成Redis類型時,遇到nil就中止轉換

來個栗子驗證一下:

EVAL "return {1,2,3.3333,'foo',nil,'bar'}" 0
1) (integer) 1
2) (integer) 2
3) (integer) 3
4) "foo"
複製代碼

能夠看到bar沒有返回,而且3.333返回了3。

腳本的原子性

Redis運行全部的Lua命令都使用相同的Lua解釋器。當一個腳本正在執行時,其餘的腳本或Redis命令都不能執行。這很像Redis的事務multi/exec。這意味着咱們要儘可能避免腳本的執行時間過長。

腳本總體複製

當腳本進行傳播或者寫入AOF文件時,Redis一般會將腳本自己進行傳播或寫入AOF,而不是使用它產生的若干命令。緣由很簡單,傳播整個腳本要比傳播一大堆生成的命令的速度要快。

從Redis3.2開始,能夠只複製影響腳本執行結果的語句,而不用複製整個腳本。這個複製整個腳本的方法有如下屬性:

  • 若是輸入相同,腳本必須輸出相同的結果。即執行結果不能依賴於隱式的變量,或依賴於I/O輸入
  • Lua不會導出訪問系統時間或其餘外部狀態的命令
  • 若是先執行了「隨機命令」(如RANDOMKEY,SRANDMEMBER,TIME),並改變了數據集,接着執行腳本時會被阻塞。
  • 在Redis4中,Lua腳本調用返回隨機順序的元素的命令時,會在返回以前進行排序,也就是說,調用redis.call("smembers",KEYS[1]),每次返回的順序都相同。從Redis5開始就不須要排序了,由於Redis5複製的是產生影響的命令。
  • Lua修改了僞隨機函數math.random和math.randomseed,使每次執行腳本時seed都相同,而若是不執行math.randomseed,只執行math.random時,每次的結果也都相同。
複製命令隊列

在這種模式下,Redis在執行腳本時會收集全部影響數據集的命令,當腳本執行完畢時,命令隊列會被放在事務中,發送給AOF文件。

Lua能夠經過執行redis.replicate_commands()函數來檢查複製模式,若是返回true表示當前是複製命令模式,若是返回false,則是複製整個腳本模式。

可選擇的複製命令

腳本複製模式選擇好之後,就能夠對複製到副本和AOF的方式進行更多的控制。這是一種高級特性,由於濫用會切斷主從備份,和AOF持久化。若是咱們只須要在master上執行某些命令時,這一特性就變得頗有用。例如咱們須要計算一些中間值時,只須要在master上計算就好,那麼這些命令就沒必要進行復制。

從Redis3.2開始,有一個新的命令叫作redis.set_repl(),它能夠用來控制複製方式,有以下選項(默認是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. 複製代碼
全局變量

爲了不數據泄露,Redis腳本不容許建立全局變量。若是必須有一個公共變量,可使用Redis的key來代替。在EVAL命令中建立一個全局變量會引發一個異常。

> eval 'a=10' 0
(error) ERR Error running script (call to f_933044db579a2f8fd45d8065f04a8d0249383e57): user_script:1: Script attempted to create global variable 'a 複製代碼
關於SELECT的使用

在Lua腳本中使用SELECT就像在正常客戶端中使用同樣。值得一提的是,在Redis2.8.12以前,Lua腳本中執行SELECT是會影響到客戶端的,而從2.8.12開始,Lua腳本中的SELECT只會在腳本執行過程當中生效。這點在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的標準庫。

在腳本中打印Redis日誌

使用redis.log(loglevel,message)函數能夠在Lua腳本中打印Redis日誌。

loglevel包括:

  • redis.LOG_DEBUG
  • redis.LOG_VERBOSE
  • redis.LOG_NOTICE
  • redis.LOG_WARNING

它們與Redis的日誌等級是對應的。

沙箱和最大執行時間

腳本不該該訪問外部系統,包括文件系統和其餘系統。腳本應該只能操做Redis數據和傳入進來的參數。

腳本默認的最大執行時間是5秒(正常腳本執行時間都是毫秒級,因此5秒已經足夠長了)。能夠經過修改lua-time-limit變量來控制最大執行時間。

當腳本執行時間超過最大執行時間時,並不會被自動終止,由於這違反了腳本的原子性原則。當一個腳本執行時間過長時,Redis會有以下操做:

  • Redis記錄下這個腳本執行時間過長
  • 其餘客戶端開始接收命令,可是全部的命令都會會返回繁忙,除了SCRIPT KILL 和 SHUTDOWN NOSAVE
  • 若是一個腳本僅執行只讀命令,則能夠用SCRIPT KILL命令來中止它。
  • 若是腳本執行了寫入命令,那麼只能用SHUTDOWN NOSAVE來終止服務器,當前的全部數據都不會保存到磁盤。

EVALSHA

最先可用版本:2.6.0

用法:EVALSHA sha1 numkeys key [key ...] arg [arg ...]

該命令用來執行緩存在服務器上的腳本,sha1爲腳本的惟一標識。

使用EVAL命令必須每次都要把腳本從客戶端傳到服務器,因爲Redis的內部緩存機制,它並不會每次都從新編譯腳本,可是傳輸上仍然浪費帶寬。

另外一方面,若是使用特殊命令或者經過redis.conf來定義命令會有如下問題:

  • 不一樣實例有不一樣的實現方式
  • 發佈將會很困難,特別是分佈式環境,由於要保證全部實例都包含給定的命令
  • 讀應用程序代碼時,因爲它調用了服務端命令,會不清楚代碼的語義

爲了不這些問題,同時避免浪費帶寬,Redis實現了EVALSHA命令。

若是服務器中沒有緩存指定的腳本,會返回給客戶端腳本不存在的錯誤信息。

SCRIPT DEBUG

最先可用版本:3.2.0

時間複雜度:O(1)

用法:SCRIPT DEBUG YES|SYNC|NO

該命令用於設置隨後執行的EVAL命令的調試模式。Redis包含一個完整的Lua調試器,代號爲LDB,可使編寫複雜腳本的任務更加簡單,在調試模式下,Redis充當遠程調試服務器,客戶端能夠逐步執行腳本,設置斷點,檢查變量等。想了解更多調試器內容的能夠查看官方文檔Redis Lua debugger

LDB能夠設置成異步或同步模式。異步模式下,服務器會fork出一個調試會話,不會阻塞主會話,,調試會話結束後,全部數據都會回滾。同步模式則會阻塞會話,並保留調試過程當中數據的改變。

SCRIPT EXISTS

最先可用版本:2.6.0

時間複雜度:O(N),N是腳本數量

返回腳本是否存在於緩存中(存在返回1,不存在返回0)。這個命令適合在管道前執行,以保證管道中的全部腳本都已經加載到服務器端了,若是沒有,須要用SCRIPT LOAD命令進行加載。

SCRIPT FLUSH

最先可用版本:2.6.0

時間複雜度:O(N),N是緩存中的腳本數

刷新緩存中的腳本,這一命令常在雲服務上被使用。

SCRIPT KILL

最先可用版本:2.6.0

時間複雜度:O(1)

中止當前正在執行的Lua腳本,一般用來中止執行時間過長的腳本。中止後,被阻塞的客戶端會拋出一個錯誤。

SCRIPT LOAD

最先可用版本:2.6.0

時間複雜度:O(N),N是腳本的字節數

該命令用於將腳本加載到服務器端的緩存中,但不會執行。加載後,服務器會一直緩存,由於良好的應用程序不太可能有太多不一樣的腳本致使內存不足。每一個腳本都像一個新命令的緩存,因此即便是大型應用程序,也就有幾百個,它們佔用的內存是微不足道的。

小結

本文介紹了Redis Lua相關的命令。其中EVAL和EVALSHA用來執行腳本。腳本執行具備原子性。腳本的複製和傳播能夠根據須要設置。腳本中不能定義全局變量。

相關文章
相關標籤/搜索