Redis Lua腳本開發之從入門到忘記

一、Lua快速入門

1.1 Lua簡介

Lua是一門簡潔小巧的腳本語言,由C語言編寫,一般用來做擴展程序,嵌入在Nginx/Redis等中間件或者其它牛逼語言中使用。

1.1.1 Lua特性

  • 輕量級:它用標準C語言編寫並以源代碼形式開放,編譯後體積很小,可以很方便的嵌入別的程序裏;
  • 可擴展:Lua提供了非常易於使用的擴展接口和機制,由宿主語言提供這些功能,Lua可以使用它們,就像是本來就內置的功能一樣;
  • 支持面向過程(procedure-oriented)編程和函數式編程(functional programming);
    自動內存管理;
  • 內置模式匹配和閉包;
  • 提供多線程(協同進程,並非操作系統所支持的線程)支持;
  • 通過閉包和table可以很方便地支持面向對象編程所需要的一些關鍵機制,比如數據抽象,虛函數,繼承和重載等;

1.1.2 應用場景

  • 遊戲開發
  • 獨立應用腳本
  • Web 應用腳本
  • 擴展和插件,如Nginx、Redis
  • 安全系統,如入侵檢測系統

1.2 環境搭建

要開發調試Lua程序,最低配的玩法就是文本編輯器+Lua解釋器/編譯器。

1.2.1 Windows安裝

https://code.google.com/archive/p/luaforwindows

1.2.2 Linux安裝

wget http://www.lua.org/ftp/lua-5.2.3.tar.gz
tar zxf lua-5.2.3.tar.gz
cd lua-5.2.3
make linux test

1.2.3 Mac安裝

curl -R -O http://www.lua.org/ftp/lua-5.2.3.tar.gz
tar zxf lua-5.2.3.tar.gz
cd lua-5.2.3
make macosx test

或者使用brew安裝:

brew install lua

1.3 Lua基本語法

1.3.1 註釋

註釋以"–"開頭,如:

--[[ my first program in Lua --]]

1.3.2 標識符

標識符用於標記變量、函數或者其它用戶定義的項目。合法的標識符包含字母、下劃線以及數字,其中數字不能位於首字符。例如:

mohd zara abc move_name a_123
myname50 _temp j a23b9 retVal

1.3.3 關鍵字

redis key words

1.3.4 變量

  1. Lua的變量包括:
  • 全局:沒有使用local定義的變量就是全局變量,b
  • 局部:使用local定義的變量,local a
  • Table成員:t[1]
  1. 變量定義
local d , f = 5 ,10 --declaration of d and f as local variables. 
d , f = 5, 10; --declaration of d and f as global variables. 
d, f = 10 --[[declaration of d and f as global variables. Here value of f is nil --]]
  1. 變量聲明
-- Variable definition:
local a, b
-- Initialization
a = 10
b = 30
print("value of a:", a)
print("value of b:", b)
-- Swapping of variables
b, a = a, b
print("value of a:", a)
print("value of b:", b)
f = 70.0/3.0
print("value of f", f)

1.3.5 數據類型

lua value types
eg.

print(type("What is my type")) --> string
t = 10
print(type(5.8*t)) --> number
print(type(true)) --> boolean
print(type(print)) --> function
print(type(nil)) --> nil
print(type(type(ABC))) --> string

1.3.6 運算符和表達式

  1. 算術運算符
    lua arithmetic operators
  2. 關係運算符
    lua relational operators
  3. 邏輯運算符
    lua logical operators
  4. 其它
    lua other operators
  5. 三目運算符
    Lua沒有提供三目運算符,但是使用邏輯運算符and和or可以實現類似效果:
value = condition and trueval or falseval;
  1. 運算符優先級
    lua operators orders

1.3.7 語句

  1. 循環
  • while loop
    lua while loop
    eg.
a = 10
while( a < 20 )
do
   print("value of a:", a)
   a = a+1
end
  • for loop
    lua for loop
    eg.
for i = 10,1,-1
do
   print(i) 
end
  • repeat … until loop
    lua repeat until loop
    eg.
--[ local variable definition --]
a = 10
--[ repeat loop execution --]
repeat
   print("value of a:", a)
   a = a + 1
until( a > 15 )
  • nested loop
    Lua的循環可以嵌套,例如:
while(condition)
do
   while(condition)
   do
      statement(s)
   end
   statement(s)
end
  • break
    可以使用break語句提前跳出循環,如下圖示:
    lua break loop
    值得注意的是,Lua中沒有標號語句,跳出多層嵌套循環時候有點不便。
  1. 分支
  • if
    lua if
    eg.
--[ local variable definition --]
a = 10;
--[ check the boolean condition using if statement --]
if( a < 20 )
then
   --[ if condition is true then print the following --]
   print("a is less than 20" );
end
print("value of a is :", a);
  • if … else
    lua if else
    eg.
--[ local variable definition --]
a = 100;
--[ check the boolean condition --]
if( a < 20 )
then
   --[ if condition is true then print the following --]
   print("a is less than 20" )
else
   --[ if condition is false then print the following --]
   print("a is not less than 20" )
end
print("value of a is :", a)
--[ local variable definition --]
a = 100
--[ check the boolean condition --]
if( a == 10 )
then
   --[ if condition is true then print the following --]
   print("Value of a is 10" )
elseif( a == 20 )
then   
   --[ if else if condition is true --]
   print("Value of a is 20" )
elseif( a == 30 )
then
   --[ if else if condition is true  --]
   print("Value of a is 30" )
else
   --[ if none of the conditions is true --]
   print("None of the values is matching" )
end
print("Exact value of a is: ", a )
  • nested if
    eg.
--[ local variable definition --]
a = 100;
b = 200;
--[ check the boolean condition --]
if( a == 100 )
then
   --[ if condition is true then check the following --]
   if( b == 200 )
   then
      --[ if condition is true then print the following --]
      print("Value of a is 100 and b is 200" );
   end
end
print("Exact value of a is :", a );
print("Exact value of b is :", b );

1.3.8 函數

  • 函數定義
optional_function_scope function function_name( argument1, argument2, argument3........, 
argumentn)
    function_body
    return result_params_comma_separated
end
  • 函數調用
    eg.
function max(num1, num2)
   if (num1 > num2) then
      result = num1;
   else
      result = num2;
   end
   return result; 
end
-- calling a function
print("The maximum of the two numbers is ",max(10,4))
print("The maximum of the two numbers is ",max(5,6))
  • 函數作爲參數
myprint = function(param)
   print("This is my print function -   ##",param,"##")
end
function add(num1,num2,functionPrint)
   result = num1 + num2
   functionPrint(result)
end
myprint(10)
add(2,5,myprint)
  • 變長參數
function average(...)
   result = 0
   local arg = {...}
   for i,v in ipairs(arg) do
      result = result + v
   end
   return result/#arg
end
print("The average is",average(10,5,3,4,5,6))
  • 返回值
    Lua的return可以返回多個值,以逗號分隔。

1.3.9 字符串

  • 字符串定義
    eg.
string1 = "Lua"
print("\"String 1 is\"",string1)
string2 = 'Tutorial'
print("String 2 is",string2)
string3 = [["Lua Tutorial"]]
print("String 3 is",string3)
  • 轉義字符
    lua escape character
  • 字符串操作
    lua string operations

1.3.10 迭代

  • 通用迭代
array = {"Lua", "Tutorial"}
for key,value in ipairs(array) 
do
   print(key, value)
end
  • 無狀態迭代

無狀態的迭代器是指不保留任何狀態的迭代器,因此在循環中我們可以利用無狀態迭代器避免創建閉包花費額外的代價。
每一次迭代,迭代函數都是用兩個變量(狀態常量和控制變量)的值作爲參數被調用,一個無狀態的迭代器只利用這兩個值可以獲取下一個元素。
這種無狀態迭代器的典型的簡單的例子是ipairs,它遍歷數組的每一個元素。

function square(iteratorMaxCount,currentNumber)
   if currentNumber<iteratorMaxCount
   then
      currentNumber = currentNumber+1
      return currentNumber, currentNumber*currentNumber
   end
     
end
for i,n in square,3,0
do
   print(i,n)
end
  • 有狀態迭代
    很多情況下,迭代器需要保存多個狀態信息而不是簡單的狀態常量和控制變量,最簡單的方法是使用閉包,還有一種方法就是將所有的狀態信息封裝到table內,將table作爲迭代器的狀態常量,因爲這種情況下可以將所有的信息存放在table內,所以迭代函數通常不需要第二個參數。
array = {"Lua", "Tutorial"}
function elementIterator (collection)
   local index = 0
   local count = #collection
     
   -- The closure function is returned
     
   return function ()
      index = index + 1
         
      if index <= count
      then
         -- return the current element of the iterator
         return collection[index]
      end
         
   end
     
end
for element in elementIterator(array)
do
   print(element)
end

1.3.11 Table

  • 定義和初始化
-- Simple empty table
mytable = {}
print("Type of mytable is ",type(mytable))
mytable[1]= "Lua"
mytable["wow"] = "Tutorial"
print("mytable Element at index 1 is ", mytable[1])
print("mytable Element at index wow is ", mytable["wow"])
-- alternatetable and mytable refers to same table
alternatetable = mytable
print("alternatetable Element at index 1 is ", alternatetable[1])
print("mytable Element at index wow is ", alternatetable["wow"])
alternatetable["wow"] = "I changed it"
print("mytable Element at index wow is ", mytable["wow"])
-- only variable released and and not table
alternatetable = nil
print("alternatetable is ", alternatetable)
-- mytable is still accessible
print("mytable Element at index wow is ", mytable["wow"])
mytable = nil
print("mytable is ", mytable)
  • 操作
    lua table operatoins

二、在Redis中的Lua

2.1 eval命令

2.1.1 指令格式

從 Redis 2.6.0 版本開始,通過內置的Lua解釋器,可以使用EVAL命令對 Lua 腳本進行求值。命令格式如下:

EVAL script numkeys key [key ...] arg [arg ...]

eg.

eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second

所有的 Redis 命令,在執行之前都會被分析,籍此來確定命令會對哪些鍵進行操作。腳本功能被設計成與集羣功能兼容,使用正確形式來傳遞KEY可以確保 Redis 集羣可以將你的請求發送到正確的集羣節點。

2.1.2 在Lua中調用Redis命令

在Lua腳本中可以使用redis.call()和redis.pcall()來執行Redis命令:

eval "return redis.call('set',KEYS[1],'bar')" 1 foo

當 redis.call() 在執行命令的過程中發生錯誤時,腳本會停止執行,並返回一個腳本錯誤,錯誤的輸出信息會說明錯誤造成的原因:

redis> lpush foo a
(integer) 1
redis> eval "return redis.call('get', 'foo')" 0
(error) ERR Error running script (call to f_282297a0228f48cd3fc6a55de6316f31422f5d17): ERR Operation against a key holding the wrong kind of value

redis.pcall() 出錯時並不引發(raise)錯誤,而是返回一個帶 err 域的 Lua 表(table),用於表示錯誤

redis 127.0.0.1:6379> EVAL "return redis.pcall('get', 'foo')" 0
(error) ERR Operation against a key holding the wrong kind of value

2.2 Lua與Redis之間的數據類型轉換

當 Lua 通過call()或pcall()函數執行 Redis 命令的時候,命令的返回值會被轉換成Lua數據結構。同樣地,當Lua腳本在Redis內置的解釋器裏運行時,Lua腳本的返回值也會被轉換成Redis協議(protocol),然後由EVAL將值返回給客戶端。

2.2.1 從Redis轉換到Lua

  • Redis integer reply -> Lua number / Redis 整數轉換成 Lua 數字
  • Redis bulk reply -> Lua string / Redis bulk 回覆轉換成 Lua 字符串
  • Redis multi bulk reply -> Lua table (may have other Redis data types nested) / Redis 多條 bulk 回覆轉換成 Lua 表,表內可能有其他別的 Redis 數據類型
  • Redis status reply -> Lua table with a single ok field containing the status / Redis 狀態回覆轉換成 Lua 表,表內的 ok 域包含了狀態信息
  • Redis error reply -> Lua table with a single err field containing the error / Redis 錯誤回覆轉換成 Lua 表,表內的 err 域包含了錯誤信息
  • Redis Nil bulk reply and Nil multi bulk reply -> Lua false boolean type / Redis 的 Nil 回覆和 Nil 多條回覆轉換成 Lua 的布爾值 false

2.2.2 從Lua轉換到Redis

  • Lua number -> Redis integer reply / Lua 數字轉換成 Redis 整數
  • Lua string -> Redis bulk reply / Lua 字符串轉換成 Redis bulk 回覆
  • Lua table (array) -> Redis multi bulk reply / Lua 表(數組)轉換成 Redis 多條 bulk 回覆
  • Lua table with a single ok field -> Redis status reply / 一個帶單個 ok 域的 Lua 表,轉換成 Redis 狀態回覆
  • Lua table with a single err field -> Redis error reply / 一個帶單個 err 域的 Lua 表,轉換成 Redis 錯誤回覆
  • Lua boolean false -> Redis Nil bulk reply / Lua 的布爾值 false 轉換成 Redis 的 Nil bulk 回覆
  • Lua boolean true -> Redis integer reply with value of 1 / Lua 布爾值 true 轉換成 Redis 整數回覆中的 1

2.3 evalsha命令

2.3.1 使用說明

爲了減少帶寬的消耗,Redis實現了EVALSHA命令,它的作用和EVAL一樣,都用於對腳本求值,但它接受的第一個參數不是腳本,而是腳本的SHA1校驗和(sum)。
如果服務器還記得給定的 SHA1 校驗和所指定的腳本,那麼執行這個腳本;如果服務器不記得給定的 SHA1 校驗和所指定的腳本,那麼它返回一個特殊的錯誤,提醒用戶使用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](/commands/eval).

2.3.2 腳本緩存

Redis 保證所有被運行過的腳本都會被永久保存在腳本緩存當中,這意味着,當EVAL命令在一個 Redis 實例上成功執行某個腳本之後,隨後針對這個腳本的所有EVALSHA命令都會成功執行。
刷新腳本緩存的唯一辦法是顯式地調用 SCRIPT FLUSH 命令,這個命令會清空運行過的所有腳本的緩存。

2.3.3 在流水線中的evalsha

在流水線請求的上下文中使用EVALSHA命令時,要特別小心,因爲一旦在流水線中因爲EVALSHA命令而發生NOSCRIPT錯誤,那麼這個流水線就再也沒有辦法重新執行了。客戶端可以採用如下措施避免:

  • 總是在流水線中使用EVAL命令
  • 檢查流水線中要用到的所有命令,找到其中的 EVAL 命令,並使用 SCRIPT EXISTS 命令檢查要用到的腳本是不是全都已經保存在緩存裏面了。如果所需的全部腳本都可以在緩存裏找到,那麼就可以放心地將所有 EVAL 命令改成 EVALSHA 命令,否則的話,就要在流水線的頂端(top)將缺少的腳本用 SCRIPT LOAD 命令加上去。

2.4 script命令

Redis 提供了以下幾個SCRIPT命令,用於對腳本子系統(scripting subsystem)進行控制:

  • SCRIPT FLUSH :清除所有腳本緩存
  • SCRIPT EXISTS :根據給定的腳本校驗和,檢查指定的腳本是否存在於腳本緩存
  • SCRIPT LOAD :將一個腳本裝入腳本緩存,但並不立即運行它
  • SCRIPT KILL :殺死當前正在運行的腳本

2.5 Redis對Lua的一些限制

2.5.1 純函數腳本

  • 對於同樣的數據集輸入,給定相同的參數,腳本執行的 Redis 寫命令總是相同的;
  • Lua 沒有訪問系統時間或者其他內部狀態的命令;
  • 每當從 Lua 腳本中調用那些返回無序元素的命令時,執行命令所得的數據在返回給 Lua 之前會先執行一個靜默(slient)的字典序排序(lexicographical sorting);
  • 對 Lua 的僞隨機數生成函數 math.random 和 math.randomseed 進行修改,使得每次在運行新腳本的時候,總是擁有同樣的 seed 值。這意味着,每次運行腳本時,只要不使用 math.randomseed ,那麼 math.random 產生的隨機數序列總是相同的;

2.5.2 全局變量保護

爲了防止不必要的數據泄漏進 Lua 環境, Redis 腳本不允許創建全局變量。如果一個腳本需要在多次執行之間維持某種狀態,它應該使用Redis key來進行狀態保存。企圖在腳本中訪問一個全局變量(不論這個變量是否存在)將引起腳本停止,EVAL命令會返回一個錯誤:

redis 127.0.0.1:6379> eval 'a=10' 0
(error) ERR Error running script (call to f_933044db579a2f8fd45d8065f04a8d0249383e57): user_script:1: Script attempted to create global variable 'a'

2.5.3 內置庫

Redis內置的Lua解釋器加載了以下Lua庫:

  • base
  • table
  • string
  • math
  • debug
  • cjson
  • cmsgpack

2.5.4 沙箱

腳本應該僅僅用於傳遞參數和對Redis數據進行處理,它不應該嘗試去訪問外部系統(比如文件系統),或者執行任何系統調用。

2.5.5 最大執行時間

腳本有一個最大執行時間限制,它的默認值是5秒鐘,由lua-time-limit選項來控制(以毫秒爲單位),可以通過編輯redis.conf文件或者使用CONFIG GET和CONFIG SET命令來修改它。
當腳本運行的時間超過最大執行時間後,以下動作會被執行:

  • Redis 記錄一個腳本正在超時運行
  • Redis 開始重新接受其他客戶端的命令請求,但是隻有 SCRIPT KILL 和 SHUTDOWN NOSAVE 兩個命令會被處理,對於其他命令請求, Redis 服務器只是簡單地返回 BUSY 錯誤;
  • 可以使用 SCRIPT KILL 命令將一個僅執行只讀命令的腳本殺死,因爲只讀命令並不修改數據,因此殺死這個腳本並不破壞數據的完整性;
  • 如果腳本已經執行過寫命令,那麼唯一允許執行的操作就是 SHUTDOWN NOSAVE ,它通過停止服務器來阻止當前數據集寫入磁盤;

三、開發調試

3.1 使用ldb調試

Redis從3.2開始提供了一個完整的Lua調試器,代號爲ldb。

3.1.1 特性

  • ldb是一個基於C/S模式開發的遠程調試器;
  • 每次debug session是一個forked session,調試過程中不會阻塞Redis接收其它客戶端的命令,調試完畢後Redis會回滾調試會話中所做的任何修改;
  • 可以使用同步模式進行調試,這樣會阻塞Redis接收其它客戶端的命令,調試過程中所做的修改也不會回滾;
  • 支持單步調試;
  • 支持靜態和動態斷點;
  • 支持打印調試日誌到命令行;
  • 可以查看和打印Lua變量取值;

3.1.2 使用說明

  1. 命令格式
./redis-cli --ldb --eval /tmp/script.lua mykey somekey , arg1 arg2

逗號前面爲key列表,逗號後面爲參數列表,注意逗號前後都需要有空格,否則會報錯。
進入到調試模式後,Redis只支持如下三個命令:

  • quit:退出調試會話,同時退出redis-cli;
  • restart:重新加載腳本,重新進入調試會話;
  • help:打印如下幫助信息:
lua debugger> help
Redis Lua debugger help:
[h]elp               Show this help.
[s]tep               Run current line and stop again.
[n]ext               Alias for step.
[c]continue          Run till next breakpoint.
[l]list              List source code around current line.
[l]list [line]       List source code around [line].
                     line = 0 means: current position.
[l]list [line] [ctx] In this form [ctx] specifies how many lines
                     to show before/after [line].
[w]hole              List all source code. Alias for 'list 1 1000000'.
[p]rint              Show all the local variables.
[p]rint <var>        Show the value of the specified variable.
                     Can also show global vars KEYS and ARGV.
[b]reak              Show all breakpoints.
[b]reak <line>       Add a breakpoint to the specified line.
[b]reak -<line>      Remove breakpoint from the specified line.
[b]reak 0            Remove all breakpoints.
[t]race              Show a backtrace.
[e]eval <code>       Execute some Lua code (in a different callframe).
[r]edis <cmd>        Execute a Redis command.
[m]axlen [len]       Trim logged Redis replies and Lua var dumps to len.
                     Specifying zero as <len> means unlimited.
[a]abort             Stop the execution of the script. In sync
                     mode dataset changes will be retained.
Debugger functions you can call from Lua scripts:
redis.debug()        Produce logs in the debugger console.
redis.breakpoint()   Stop execution as if there was a breakpoint in the
                     next line of code.
  1. 斷點
    在控制檯輸入b 3,就可以在腳本的第三行打上斷點。
    在Lua腳本中還可以使用動態斷點:
if counter > 10 then redis.breakpoint() end

在調試中,輸入s可以單步執行,使用c可以執行到下一個斷點位置。
3. 同步模式
加上–ldb-sync-mode即可進入同步調試模式。
4. 打印變量
在調試命令行中輸入p var可以在控制檯中打印出變量var的取值。
5. 執行命令
在調試中,還可以輸入e執行一些Lua腳本,不過會在另外一個調用幀中執行,因爲調試會話是一個forked session。

lua debugger> e redis.sha1hex('foo')
<retval> "0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33"

3.2 使用ZeroBrane Studio調試

ZeroBrane是一個免費、開源、跨平臺的Lua IDE,可以很方便地開發調試Lua腳本。

3.2.1 安裝ZeroBrane

打開如下頁面下載對應平臺的源文件並安裝:
https://studio.zerobrane.com/download?not-this-time

3.2.2 安裝Redis插件

下載插件
https://github.com/pkulchenko/ZeroBranePackage/blob/master/redis.lua
然後放置到ZeroBrane安裝目錄的packages(/opt/zbstudio/packages/)下,或者是~/.zbstudio/packages文件夾中。

3.2.3 使用ZeroBrane調試Redis Lua腳本

打開IDE,新建一個Lua腳本,然後Project -> Lua Interpreter,選擇Redis
zerobrane new lua project
然後Project -> Command Line Parameters…,輸入對應的key和參數,注意逗號前後都要有空格(這裏實際上執行的就是Redis的eval命令)。
zerobrane command line params  1
zerobrane command line params 2
然後點擊運行,ZeroBrane會提示輸入redis地址
zerobrane enter redis addr
可以設置斷點,查看變量和單步執行
zerobrane break points
ZeroBrane還提供了Remote Console用於遠程執行Lua腳本和Redis命令(全大寫或者用@開頭的小寫Redis命令):
zero redis commands

四、參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章