Redis Lua 腳本

Lua 簡介

Lua語言提供了以下幾種數據類型:booleans(布爾)、numbers(數值)、strings(字符串)、tables(表格)。html

下面是一些 Lua 的示例,裏面註釋部分會講解相關的做用:python

--
--
-- 拿客 
-- 網站:www.coderknock.com 
-- QQ羣:213732117
-- 三產 建立於 2017年06月15日 12:04:54。
-- 描述:
--
--
local strings website = "coderknock.com"
print(website)
local tables testArray = { website, "sanchan", true, 2, 3.1415926 }

-- 遍歷 testArray
print("========  testArray =======")
for i = 1, #testArray
do
    print(testArray[i])
end
-- 另外一種遍歷方式
print("======== in  testArray =======")
for index, value in ipairs(testArray)
do
    -- 這種方式拼接 boolean 是會報錯
    print("index ---->"..index)
    -- 這種組合大量數據時效率高
    print(value)
end

--while 循環
print("======== while =======")
local int sum = 0
local int i = 0
while i <= 100
do
    sum = sum +i
    i = i + 1
end
--輸出結果爲5050
print(sum)

--if else
print("======== if else =======")
for i = 1, #testArray
do
    if testArray[i] == "sanchan"
    then
        print("true")
        break
    else
        print(testArray[i])
    end
end

-- 哈希
local tables user_1 = { age = 28, name = "tome" }
--user_1 age is 28
print("======== hash =======")
print(user_1["name"].." age is " .. user_1["age"])
print("======== in hash =======")
for key, value in pairs(user_1)
do
    print(key .. ":".. value)
end

print("======== function =======")
function funcName(str)
    -- 代碼邏輯
    print(str)
    return "new"..str
end

print(funcName("123"))

Redis 中執行 Lua 腳本

Lua腳本功能爲Redis開發和運維人員帶來以下三個好處:web

  • Lua腳本在Redis中是原子執行的,執行過程當中間不會插入其餘命令。redis

  • Lua腳本能夠幫助開發和運維人員創造出本身定製的命令,並能夠將這些命令常駐在Redis內存中,實現複用的效果。shell

  • Lua腳本能夠將多條命令一次性打包,有效地減小網絡開銷。數據庫

EVAL

自2.6.0可用。編程

時間複雜度:EVAL 和 EVALSHA 能夠在 O(1) 複雜度內找到要被執行的腳本,其他的複雜度取決於執行的腳本自己。json

語法:EVAL script numkeys key [key ...] arg [arg ...]
說明:

從 Redis 2.6.0 版本開始,經過內置的 Lua 解釋器,可使用 EVAL 命令對 Lua 腳本進行求值。segmentfault

script 參數是一段 Lua 5.1 腳本程序,它會被運行在 Redis 服務器上下文中,這段腳本沒必要(也不該該)定義爲一個 Lua 函數。數組

numkeys 參數用於指定鍵名參數的個數。

鍵名參數 key [key ...]EVAL 的第三個參數開始算起,表示在腳本中所用到的那些 Redis 鍵(key),這些鍵名參數能夠在 Lua 中經過全局變量 KEYS 數組,用 1 爲起始全部的形式訪問( KEYS[1]KEYS[2] ,以此類推)。

在命令的最後是那些不是鍵名參數的附加參數 arg [arg ...] ,能夠在 Lua 中經過全局變量 ARGV 數組訪問,訪問的形式和 KEYS 變量相似( ARGV[1]ARGV[2] ,諸如此類)。

上面這幾段長長的說明能夠用一個簡單的例子來歸納:

coderknock>EVAL 'return "return String KEYS1: "..KEYS[1].." KEYS2: ".." "..KEYS[2].." ARGV1: "..ARGV[1].." ARGV2: "..ARGV[2]' 3 KEYS1Str KEYS2Str KEYS3Str ARGV1Str ARGV2Str ARGV3Str ARGV4Str
"return String KEYS1: KEYS1Str KEYS2:  KEYS2Str ARGV1: ARGV1Str ARGV2: ARGV2Str"

在 Lua 腳本中,可使用兩個不一樣函數來執行 Redis 命令,它們分別是:

  • redis.call()

  • redis.pcall()

這兩個函數的惟一區別在於它們使用不一樣的方式處理執行命令所產生的錯誤,在後面的『錯誤處理』部分會講到這一點。

redis.call()redis.pcall() 兩個函數的參數能夠是任何格式良好(well formed)的 Redis 命令:

# 最後的 0 表明的是沒有 keys 是必須的 
127.0.0.1:6379> EVAL "return redis.call('SET','testLuaSet','luaSetValue')" 0
OK
127.0.0.1:6379> GET testLuaSet
"luaSetValue"
127.0.0.1:6379> EVAL "return redis.call('GET','testLuaSet')" 0
"luaSetValue"

上面的腳本雖然完成了功能,可是 key 部分應該由 Redis 傳入而不是在 Lua 腳本中直接寫入,咱們改進一下:

127.0.0.1:6379>  EVAL "return redis.call('SET',KEYS[1],ARGV[1])" 1 evalShell shellTest
OK
127.0.0.1:6379> GET evalShell
"shellTest"

下面咱們再次改進運行多 key 插入,這裏使用 Python :

import redis

r = redis.StrictRedis(host='127.0.0.1', password='admin123', port=6379, db=0)
luaScript = """
for i = 1, #KEYS
do
    redis.call('SET', KEYS[i], ARGV[i])
end
return #KEYS
"""
luaSet = r.register_script(luaScript)
luaSet(keys=["pyLuaKey1", "pyLuaKey2", "pyLuaKey3"], args=["pyLuaKeyArg1", "pyLuaKeyArg2", "pyLuaKeyArg3"])
# r.eval(luaScript,)
# 下面會報錯 由於 ARGV 會數組越界
# luaSet(keys=["key1", "key2", "key3"], args=["arg1"])

咱們在終端中驗證一下是否插入成功:

127.0.0.1:6379> GET pyLuaKey1
"pyLuaKeyArg1"
127.0.0.1:6379> GET pyLuaKey2
"pyLuaKeyArg2"
127.0.0.1:6379> GET pyLuaKey3
"pyLuaKeyArg3"

要求使用正確的形式來傳遞鍵(key)是有緣由的,由於不只僅是 EVAL 這個命令,全部的 Redis 命令,在執行以前都會被分析,籍此來肯定命令會對哪些鍵進行操做。

所以,對於 EVAL 命令來講,必須使用正確的形式來傳遞鍵,才能確保分析工做正確地執行。除此以外,使用正確的形式來傳遞鍵還有不少其餘好處,它的一個特別重要的用途就是確保 Redis 集羣能夠將你的請求發送到正確的集羣節點。(對 Redis 集羣的工做還在進行當中,可是腳本功能被設計成能夠與集羣功能保持兼容。)不過,這條規矩並非強制性的,從而使得用戶有機會濫用(abuse) Redis 單實例配置(single instance configuration),代價是這樣寫出的腳本不能被 Redis 集羣所兼容。

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

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

數據類型之間的轉換遵循這樣一個設計原則:若是將一個 Redis 值轉換成 Lua 值,以後再將轉換所得的 Lua 值轉換回 Redis 值,那麼這個轉換所得的 Redis 值應該和最初時的 Redis 值同樣。

換句話說, Lua 類型和 Redis 類型之間存在着一一對應的轉換關係。

如下列出的是詳細的轉換規則:

從 Redis 轉換到 Lua :

  • Redis 整數轉換成 Lua numbers

  • Redis bulk 回覆轉換成 Lua strings

  • Redis 多條 bulk 回覆轉換成 Lua tables,tables 內可能有其餘別的 Redis 數據類型

  • Redis 狀態回覆轉換成 Lua tables, tables 內的 ok 域包含了狀態信息

  • Redis 錯誤回覆轉換成 Lua tables ,tables 內的 err 域包含了錯誤信息

  • Redis 的 Nil 回覆和 Nil 多條回覆轉換成 Lua 的 booleans false

從 Lua 轉換到 Redis:

  • Lua numbers 轉換成 Redis 整數

  • Lua strings 換成 Redis bulk 回覆

  • Lua tables (array) 轉換成 Redis 多條 bulk 回覆

  • 一個帶單個 ok 域的 Lua tables,轉換成 Redis 狀態回覆

  • 一個帶單個 err 域的 Lua tables ,轉換成 Redis 錯誤回覆

  • Lua 的 booleans false 轉換成 Redis 的 Nil bulk 回覆

從 Lua 轉換到 Redis 有一條額外的規則,這條規則沒有和它對應的從 Redis 轉換到 Lua 的規則:

  • Lua booleans true 轉換成 Redis 整數回覆中的 1

如下是幾個類型轉換的例子:

# Lua strings  換成 Redis bulk 回覆
127.0.0.1:6379> EVAL "return redis.call('GET','evalShell')" 0
"shellTest"
# 錯誤的狀況
127.0.0.1:6379>  EVAL "return redis.call('SADD','evalShell','a')" 0

(error) ERR Error running script (call to f_e17faafbc130014cebb229b71e0148b1f8f52389): @user_script:1: WRONGTYPE Operation against a key holding the wrong kind of value
# redis 中與 lua 各類類型轉換
127.0.0.1:6379> EVAL "return {1,3.1415,'luaStrings',true,false}" 0
1) (integer) 1
2) (integer) 3
3) "luaStrings"
4) (integer) 1
5) (nil)
腳本的原子性

Redis 使用單個 Lua 解釋器去運行全部腳本,而且, Redis 也保證腳本會以原子性(atomic)的方式執行:當某個腳本正在運行的時候,不會有其餘腳本或 Redis 命令被執行。這和使用 MULTI / EXEC 包圍的事務很相似。在其餘別的客戶端看來,腳本的效果(effect)要麼是不可見的(not visible),要麼就是已完成的(already completed)。

另外一方面,這也意味着,執行一個運行緩慢的腳本並非一個好主意。寫一個跑得很快很順溜的腳本並不難,由於腳本的運行開銷(overhead)很是少,可是當你不得不使用一些跑得比較慢的腳本時,請當心,由於當這些蝸牛腳本在慢吞吞地運行的時候,其餘客戶端會由於服務器正忙而沒法執行命令。

錯誤處理

前面的命令介紹部分說過, redis.call()redis.pcall() 的惟一區別在於它們對錯誤處理的不一樣。

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

127.0.0.1:6379>  EVAL "return redis.call('SADD','evalShell','a')" 0

(error) ERR Error running script (call to f_e17faafbc130014cebb229b71e0148b1f8f52389): @user_script:1: WRONGTYPE Operation against a key holding the wrong kind of value

redis.call() 不一樣, redis.pcall() 出錯時並不引起(raise)錯誤,而是返回一個帶 err 域的 Lua 表(table),用於表示錯誤(這樣與命令行客戶端直接操做返回相同):

127.0.0.1:6379>  EVAL "return redis.pcall('SADD','evalShell','a')" 0
(error) WRONGTYPE Operation against a key holding the wrong kind of value
Helper 函數返回Redis類型

從Lua返回Redis類型有兩個 Helper 函數。

  • redis.error_reply(error_string)返回錯誤回覆。此函數只返回一個字段表,其中err字段設置爲指定的字符串。

  • redis.status_reply(status_string)返回狀態回覆。此函數只返回一個字段表,其中ok字段設置爲指定的字符串。

使用 Helper 函數或直接以指定的格式返回表之間沒有區別,所以如下兩種形式是等效的:

return {err="My Error"}
return redis.error_reply("My Error")

腳本緩存

Redis 保證全部被運行過的腳本都會被永久保存在腳本緩存當中,這意味着,當 EVAL 命令在一個 Redis 實例上成功執行某個腳本以後,隨後針對這個腳本的全部 EVALSHA 命令都會成功執行。

刷新腳本緩存的惟一辦法是顯式地調用 SCRIPT FLUSH 命令,這個命令會清空運行過的全部腳本的緩存。一般只有在雲計算環境中,Redis 實例被改做其餘客戶或者別的應用程序的實例時,纔會執行這個命令。

緩存能夠長時間儲存而不產生內存問題的緣由是,它們的體積很是小,並且數量也很是少,即便腳本在概念上相似於實現一個新命令,即便在一個大規模的程序裏有成百上千的腳本,即便這些腳本會常常修改,即使如此,儲存這些腳本的內存仍然是微不足道的。

事實上,用戶會發現 Redis 不移除緩存中的腳本其實是一個好主意。好比說,對於一個和 Redis 保持持久化連接(persistent connection)的程序來講,它能夠確信,執行過一次的腳本會一直保留在內存當中,所以它能夠在 pipline 中使用 EVALSHA 命令而沒必要擔憂由於找不到所需的腳本而產生錯誤(稍候咱們會看到在 pipline 中執行腳本的相關問題)。

SCRIPT 命令

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

SCRIPT LOAD

自2.6.0可用。

時間複雜度:O(N) , N 爲腳本的長度(以字節爲單位)。

語法:SCRIPT LOAD script
說明:

清除全部 Lua 腳本緩存。

返回值:

給定 script 的 SHA1 校驗和

SCRIPT DEBUG

自3.2.0可用。

時間複雜度:O(1)。

語法:SCRIPT DEBUG YES|SYNC|NO
說明:

Redis包括一個完整的 Lua 調試器,代號 LDB,可用於使編寫複雜腳本的任務更簡單。在調試模式下,Redis 充當遠程調試服務器,客戶端 redis-cli 能夠逐步執行腳本,設置斷點,檢查變量等 。

應避免施工生產機器進行調試!

LDB能夠以兩種模式之一啓用:異步或同步。在異步模式下,服務器建立一個不阻塞的分支調試會話,而且在會話完成後,數據的全部更改都將回滾,所以可使用相同的初始狀態從新啓動調試。同步調試模式在調試會話處於活動狀態時阻塞服務器,而且數據集在結束後會保留全部更改。

  • YES。啓用Lua腳本的非阻塞異步調試(更改將被丟棄)。

  • SYNC。啓用阻止Lua腳本的同步調試(保存對數據的更改)。

  • NO。禁用腳本調試模式。

返回值:

老是返回 OK

示例:

該功能是新出功能,使用頻率不是很高,在以後我會單獨錄個視頻來進行演示(請關注個人博客 www.coderknock.com,或關注本文後續更新)。

SCRIPT FLUSH

自2.6.0可用。

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

語法:SCRIPT FLUSH
說明:

清除全部 Lua 腳本緩存。

返回值:

老是返回 OK

SCRIPT EXISTS

自2.6.0可用。

時間複雜度:O(N) , N 爲給定的 SHA1 校驗和的數量。

語法:SCRIPT EXISTS sha1 [sha1 ...]
說明:

給定一個或多個腳本的 SHA1 校驗和,返回一個包含 01 的列表,表示校驗和所指定的腳本是否已經被保存在緩存當中。

返回值:

一個列表,包含 01 ,前者表示腳本不存在於緩存,後者表示腳本已經在緩存裏面了。

列表中的元素和給定的 SHA1 校驗和保持對應關係,好比列表的第三個元素的值就表示第三個 SHA1 校驗和所指定的腳本在緩存中的狀態。

SCRIPT KILL

自2.6.0可用。

時間複雜度:O(1)。

語法:SCRIPT KILL
說明:

殺死當前正在運行的 Lua 腳本,當且僅當這個腳本沒有執行過任何寫操做時,這個命令才生效。

這個命令主要用於終止運行時間過長的腳本,好比一個由於 BUG 而發生無限 loop 的腳本,諸如此類。

SCRIPT KILL 執行以後,當前正在運行的腳本會被殺死,執行這個腳本的客戶端會從 EVAL 命令的阻塞當中退出,並收到一個錯誤做爲返回值。

另外一方面,假如當前正在運行的腳本已經執行過寫操做,那麼即便執行 SCRIPT KILL ,也沒法將它殺死,由於這是違反 Lua 腳本的原子性執行原則的。在這種狀況下,惟一可行的辦法是使用 SHUTDOWN NOSAVE 命令,經過中止整個 Redis 進程來中止腳本的運行,並防止不完整(half-written)的信息被寫入數據庫中。

返回值:

執行成功返回 OK ,不然返回一個錯誤。

SCRIPT 相關示例:

# 加載一個腳本到緩存
127.0.0.1:6379> SCRIPT LOAD "return redis.call('SET',KEYS[1],ARGV[1])"
"cf63a54c34e159e75e5a3fe4794bb2ea636ee005"
# EVALSHA 在後面會講解,這裏就是調用一個腳本緩衝
127.0.0.1:6379> EVALSHA cf63a54c34e159e75e5a3fe4794bb2ea636ee005 1 ttestScript evalSHATest
OK
127.0.0.1:6379> GET ttestScript
"evalSHATest"
127.0.0.1:6379> SCRIPT EXISTS cf63a54c34e159e75e5a3fe4794bb2ea636ee005
1) (integer) 1
# 這裏有三個 SHA 第一第三是隨便輸入的,檢測是否存在腳本緩存
127.0.0.1:6379> SCRIPT EXISTS  nonsha cf63a54c34e159e75e5a3fe4794bb2ea636ee005 abc
1) (integer) 0
2) (integer) 1
3) (integer) 0
# 清空腳本緩存
127.0.0.1:6379> SCRIPT FLUSH
OK
# 清除腳本緩存後再次執行就找不到該腳本了
127.0.0.1:6379>  SCRIPT EXISTS cf63a54c34e159e75e5a3fe4794bb2ea636ee005
1) (integer) 0

# 沒有腳本在執行時
127.0.0.1:6379> SCRIPT KILL
(error) ERR No scripts in execution right now.

咱們建立一個 lua 腳本,該腳本存在 E:/LuaProject/src/Sleep.lua :

--
--
-- 拿客 
-- 網站:www.coderknock.com 
-- QQ羣:213732117
-- 三產 建立於 2017年06月16日 15:47:30。
-- 描述:
--
--

for i = 1, 1000000
do
    print(i)--就是循環打印這樣能夠模擬長時間的腳本執行
end
return "ok"

使用 redis-cli --eval 調用:

C:\Users\zylia>redis-cli -a admin123 --eval E:/LuaProject/src/Sleep.lua

此時服務端開始輸出,當前客戶端被阻塞:

1
2
3
...
23456

咱們再啓動一個客戶端:

C:\Users\zylia>redis-cli -a admin123
# 殺掉還在執行的那個腳本
127.0.0.1:6379> SCRIPT KILL
OK
(0.84s)

此時剛纔咱們執行腳本的客戶端(就是被阻塞的那個)會拋出異常:

(error) ERR Error running script (call to f_d5ee0fe7467b0e19fe3fb0a0388d522bf26d95d8): @user_script:13: Script killed by user with SCRIPT KILL...

服務端也會中止打印:

524991
524992
524993
524994
524995
524996
524997
524998
[1904] 16 Jun 15:55:24.178 # Lua script killed by user with SCRIPT KILL.

帶寬和 EVALSHA

EVAL 命令要求你在每次執行腳本的時候都發送一次腳本主體(script body)。Redis 有一個內部的緩存機制,所以它不會每次都從新編譯腳本,不過在不少場合,付出無謂的帶寬來傳送腳本主體並非最佳選擇。

爲了減小帶寬的消耗, Redis 實現了 EVALSHA 命令,它的做用和 EVAL 同樣,都用於對腳本求值,但它接受的第一個參數不是腳本,而是腳本的 SHA1 校驗和(sum)。

EVALSHA 命令的表現以下:

  • 若是服務器還記得給定的 SHA1 校驗和所指定的腳本,那麼執行這個腳本

  • 若是服務器不記得給定的 SHA1 校驗和所指定的腳本,那麼它返回一個特殊的錯誤,提醒用戶使用 EVAL 代替 EVALSHA

我將以前的腳本存儲到緩存中使用 EVALSHA 調用:

127.0.0.1:6379> SCRIPT LOAD "return redis.call('GET','evalShell')"
"c870035beb27b1c404c19624c50b5e451ecf1623"
127.0.0.1:6379> EVALSHA c870035beb27b1c404c19624c50b5e451ecf1623 0
"shellTest"
127.0.0.1:6379> evalsha 6b1bf486c81ceb7edf3c093f4c48582e38c0e791 0
(nil)
127.0.0.1:6379> evalsha ffffffffffffffffffffffffffffffffffffffff 0
(error) NOSCRIPT No matching script. Please use EVAL.
# 在 EVAL 錯誤的狀況中 call to f_e17faafbc130014cebb229b71e0148b1f8f52389
# e17faafbc130014cebb229b71e0148b1f8f52389 就是該命令的 SHA1 值
127.0.0.1:6379>  EVAL "return redis.call('SADD','evalShell','a')" 0

(error) ERR Error running script (call to f_e17faafbc130014cebb229b71e0148b1f8f52389): @user_script:1: WRONGTYPE Operation against a key holding the wrong kind of value

純函數腳本

在編寫腳本方面,一個重要的要求就是,腳本應該被寫成純函數(pure function)。

也就是說,腳本應該具備如下屬性:

對於一樣的數據集輸入,給定相同的參數,腳本執行的 Redis 寫命令老是相同的。腳本執行的操做不能依賴於任何隱藏(非顯式)數據,不能依賴於腳本在執行過程當中、或腳本在不一樣執行時期之間可能變動的狀態,而且它也不能依賴於任何來自 I/O 設備的外部輸入。
使用系統時間(system time),調用像 RANDOMKEY 那樣的隨機命令,或者使用 Lua 的隨機數生成器,相似以上的這些操做,都會形成腳本的求值沒法每次都得出一樣的結果。

爲了確保腳本符合上面所說的屬性, Redis 作了如下工做:

Lua 沒有訪問系統時間或者其餘內部狀態的命令

  • Redis 會返回一個錯誤,阻止這樣的腳本運行: 這些腳本在執行隨機命令以後(好比 RANDOMKEYSRANDMEMBERTIME 等),還會執行能夠修改數據集的 Redis 命令。若是腳本只是執行只讀操做,那麼就沒有這一限制。注意,隨機命令並不必定就指那些帶 RAND 字眼的命令,任何帶有非肯定性的命令都會被認爲是隨機命令,好比 TIME 命令就是這方面的一個很好的例子。

  • 每當從 Lua 腳本中調用那些返回無序元素的命令時,執行命令所得的數據在返回給 Lua 以前會先執行一個靜默(slient)的字典序排序(lexicographical sorting)。舉個例子,由於 Redis 的 Set 保存的是無序的元素,因此在 Redis 命令行客戶端中直接執行 SMEMBERS ,返回的元素是無序的,可是,假如在腳本中執行 redis.call("smembers", KEYS[1]) ,那麼返回的老是排過序的元素。

  • 對 Lua 的僞隨機數生成函數 math.randommath.randomseed 進行修改,使得每次在運行新腳本的時候,老是擁有一樣的 seed 值。這意味着,每次運行腳本時,只要不使用 math.randomseed,那麼 math.random 產生的隨機數序列老是相同的。

當 Redis 執行 Lua 腳本時會對腳本進行檢查,要執行的 lua 腳本:

function fun()
  -- 業務邏輯
end

執行是報錯,由於 Redis 不容許腳本中存在 function:

C:\Users\zylia>redis-cli -a admin123 --eval E:/LuaProject/src/Sleep.lua
(error) ERR Error running script (call to f_36ebb6a8391764938e347056b2de7a33626c029b): @enable_strict_lua:8: user_script:11: Script attempted to create global variable 'fun'

要執行的 lua 腳本:

for i = 1, 100
do
    os.execute("ping -n " .. tonumber(2) .. " localhost > NUL")
    print(i)
end
return "ok"

執行是報錯,由於 Redis 不容許腳本使用 os 等一部分全局變量:

C:\Users\zylia>redis-cli -a admin123 --eval E:/LuaProject/src/Sleep.lua
(error) ERR Error running script (call to f_bb4268eafae9d9bcd8a2571f067abf5ab46be3d0): @enable_strict_lua:15: user_script:13: Script attempted to access unexisting global variable 'os'

全局變量保護

爲了防止沒必要要的數據泄漏進 Lua 環境, Redis 腳本不容許建立全局變量。若是一個腳本須要在屢次執行之間維持某種狀態,它應該使用 Redis key 來進行狀態保存。

企圖在腳本中訪問一個全局變量(不論這個變量是否存在)將引發腳本中止, EVAL 命令會返回一個錯誤:

127.0.0.1:6379> EVAL "website='coderknock.com'" 0
(error) ERR Error running script (call to f_ad03e14e835e9880720cd43db8062256c089cd79): @enable_strict_lua:8: user_script:1: Script attempted to create global variable 'website'

Lua 的 debug 工具,或者其餘設施,好比打印(alter)用於實現全局保護的 meta table ,均可以用於實現全局變量保護。

實現全局變量保護並不難,不過有時候仍是會不當心而爲之。一旦用戶在腳本中混入了 Lua 全局狀態,那麼 AOF 持久化和複製(replication)都會沒法保證,因此,請不要使用全局變量。

避免引入全局變量的一個訣竅是:將腳本中用到的全部變量都使用 local 關鍵字定義爲局部變量。

Redis 內置的 Lua 解釋器加載瞭如下 Lua 庫:

  • base

  • table

  • string

  • math

  • debug

  • cjson

  • cmsgpack

其中 cjson 庫可讓 Lua 以很是快的速度處理 JSON 數據,除此以外,其餘別的都是 Lua 的標準庫。

每一個 Redis 實例都保證會加載上面列舉的庫,從而確保每一個 Redis 腳本的運行環境都是相同的。

下面咱們演示一下 cjson 的使用,Lua 腳本以下

--
--
-- 拿客 
-- 網站:www.coderknock.com 
-- QQ羣:213732117
-- 三產 建立於 2017年06月16日 15:47:30。
-- 描述:
--
--

local json = cjson
local str = '["testWebsit", "testQQ", "sanchan"]'   -- json格式的字符串
local j = json.decode(str)      -- 解碼爲表
for i = 1, #j
do
    print(i.." --> "..j[i])
end
str = '{"WebSite": "coderknock.com", "QQGroup": 213732117}'
j = json.decode(str)

j['Auth'] = 'sachan'
local new_str = json.encode(j)
return new_str

執行過程以下,上面的命令窗口是咱們的客戶端,下面是 Redis:

cjson 執行結果

能夠看到,客戶端輸出了一個序列號 json ,服務端打印出來咱們解碼的 json。

使用腳本散發 Redis 日誌

在 Lua 腳本中,能夠經過調用 redis.log 函數來寫 Redis 日誌(log):

redis.log(loglevel, message)

其中, message 參數是一個字符串,而 loglevel 參數能夠是如下任意一個值:

redis.LOG_DEBUG
redis.LOG_VERBOSE
redis.LOG_NOTICE
redis.LOG_WARNING
上面的這些等級(level)和標準 Redis 日誌的等級相對應。

對於腳本散發(emit)的日誌,只有那些和當前 Redis 實例所設置的日誌等級相同或更高級的日誌纔會被散發。

如下是一個日誌示例:

redis.log(redis.LOG_WARNING, "Something is wrong with this script.")

執行上面的函數會在服務器端產生這樣的信息:

[32343] 22 Mar 15:21:39 # Something is wrong with this script.

沙箱(sandbox)和最大執行時間

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

除此以外,腳本還有一個最大執行時間限制,它的默認值是 5 秒鐘,通常正常運做的腳本一般能夠在幾分之幾毫秒以內完成,花不了那麼多時間,這個限制主要是爲了防止因編程錯誤而形成的無限循環而設置的。

最大執行時間的長短由 lua-time-limit 選項來控制(以毫秒爲單位),能夠經過編輯 redis.conf 文件或者使用 CONFIG GET 和 CONFIG SET 命令來修改它。

當一個腳本達到最大執行時間的時候,它並不會自動被 Redis 結束,由於 Redis 必須保證腳本執行的原子性,而中途中止腳本的運行意味着可能會留下未處理完的數據在數據集(data set)裏面。

所以,當腳本運行的時間超過最大執行時間後,如下動做會被執行:

  • Redis 記錄一個腳本正在超時運行

  • Redis 開始從新接受其餘客戶端的命令請求,可是隻有 SCRIPT KILL 和 SHUTDOWN NOSAVE 兩個命令會被處理,對於其餘命令請求, Redis 服務器只是簡單地返回 BUSY 錯誤。

  • 可使用 SCRIPT KILL 命令將一個僅執行只讀命令的腳本殺死,由於只讀命令並不修改數據,所以殺死這個腳本並不破壞數據的完整性

  • 若是腳本已經執行過寫命令,那麼惟一容許執行的操做就是 SHUTDOWN NOSAVE ,它經過中止服務器來阻止當前數據集寫入磁盤

pipeline上下文(context)中的 EVALSHA

在 pipeline 請求的上下文中使用 EVALSHA 命令時,要特別當心,由於在 pipeline 中,必須保證命令的執行順序。

一旦在 pipeline 中由於 EVALSHA 命令而發生 NOSCRIPT 錯誤,那麼這個 pipeline 就再也沒有辦法從新執行了,不然的話,命令的執行順序就會被打亂。

爲了防止出現以上所說的問題,客戶端庫實現應該實施如下的其中一項措施:

  • 老是在 pipeline 中使用 EVAL 命令

  • 檢查 pipeline 中要用到的全部命令,找到其中的 EVAL 命令,並使用 SCRIPT EXISTS 命令檢查要用到的腳本是否是全都已經保存在緩存裏面了。若是所需的所有腳本均可以在緩存裏找到,那麼就能夠放心地將全部 EVAL 命令改爲 EVALSHA 命令,不然的話,就要在pipeline 的頂端(top)將缺乏的腳本用 SCRIPT LOAD 命令加上去。

我是廣告

本人的直播課程在 7 月份就要開始了,但願小夥伴們支持一下,如今報名有優惠噢

https://segmentfault.com/l/15...

https://segmentfault.com/l/15...

相關文章
相關標籤/搜索