Redis Lua 腳本

Redis 使用 Lua 的好處

Lua 簡介就不復制了.

Redis 提供了很是富的命令集, 可是用戶依然不知足, 但願能夠自定義擴充若干指令來完成一些特定領域的問題.redis

Redis 爲這樣的用戶場景提供了 lua 腳本支持, 用戶能夠向服務器發送 lua 腳原本執行自定義動做, 獲取腳本的響應數據. Redis 服務器會單線程原子性執行 lua 腳本, 保證 lua 腳本在處理的過程當中不會被任意其它請求打斷.數據庫

對於這點很是像事務, 事務是須要先將命令發給一條一條的發送給 Redis, 而後調用 EXEC 執行事務. 而 Lua 腳本, 能夠在服務端直接執行, 因此相應的也減小了網絡帶寬.編程

clipboard.png

Redis 中 Lua 腳本相關命令

SCRIPT LOAD 命令

SCRIPT LOAD script

腳本 script 添加到腳本緩存中, 但並不當即執行這個腳本.數組

返回給定 script 的 SHA1 校驗和.緩存

一個最簡單的例子:服務器

127.0.0.1:6379> SCRIPT LOAD "return 'hello moto'"
"232fd51614574cf0867b83d384a5e898cfd24e5a"
127.0.0.1:6379> EVALSHA 232fd51614574cf0867b83d384a5e898cfd24e5a 0
"hello moto"
值得注意的是:
若是給定的腳本已經在緩存裏面了, 那麼不作動做.
腳本能夠在緩存中保留無限長的時間, 直到執行 SCRIPT FLUSH 爲止.

SCRIPT FLUSH 命令

SCRIPT FLUSH

這個命令沒啥好說的, 就是清除全部 Lua 腳本緩存.網絡

返回值老是返回 OK函數

EVALSHA 命令

EVALSHA sha1 numkeys key [key ...] arg [arg ...]

根據給定的 sha1 校驗碼, 對緩存在服務器中的腳本進行執行.工具

EVAL 命令

EVAL script numkeys key [key …] arg [arg …]

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], 諸如此類).

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

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

其中 "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 是被求值的 Lua 腳本, 數字 2 指定了鍵名參數的數量, key1key2 是鍵名參數, 分別使用 KEYS[1]KEYS[2] 訪問, 而最後的 firstsecond 則是附加參數, 能夠經過 ARGV[1]ARGV[2] 訪問它們.

鍵名參數 能夠理解爲, 腳本可能讀取或寫入的鍵.
附加參數 能夠理解爲, 邏輯判斷條件或要寫入的數據.

在 Lua 腳本中執行 Redis 命令.

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

  • redis.call()
  • redis.pcall()

好比下面的這段腳本:

if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

使用 EVAL 命令執行:

127.0.0.1:6379> set foo bar
OK
127.0.0.1:6379> eval 'if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end' 1 foo bar
(integer) 1

SCRIPT EXISTS 命令

SCRIPT EXISTS sha1 [sha1 …]

判斷一個或多個腳本的 SHA1 校驗和, 是否已經添加到腳本緩存中.

存在返回1, 不存在返回0.

127.0.0.1:6379> SCRIPT EXISTS 232fd51614574cf0867b83d384a5e898cfd24e5a 232fd51614574cf0867b83d384a5e898cfd24e5b
1) (integer) 1
2) (integer) 0

SCRIPT KILL 命令

SCRIPT KILL

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

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

另外一方面, 假如當前正在運行的腳本已經執行過寫操做, 那麼即便執行 SCRIPT KILL, 也沒法將它殺死, 由於這是違反 Lua 腳本的原子性執行原則的.

在這種狀況下, 惟一可行的辦法是使用 SHUTDOWN NOSAVE 命令, 經過中止整個 Redis 進程來中止腳本的運行, 並防止不完整 (half-written) 的信息被寫入數據庫中.

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

# 沒有腳本在執行時

redis> SCRIPT KILL
(error) ERR No scripts in execution right now.

# 成功殺死腳本時

redis> SCRIPT KILL
OK
(1.30s)

# 嘗試殺死一個已經執行過寫操做的腳本,失敗

redis> SCRIPT KILL
(error) ERR Sorry the script already executed write commands against the dataset. You can either wait the script termination or kill the server in an hard way using the SHUTDOWN NOSAVE command.
(1.69s)

如下是腳本被殺死以後, 返回給執行腳本的客戶端的錯誤:

redis> EVAL "while true do end" 0
(error) ERR Error running script (call to f_694a5fe1ddb97a4c6a1bf299d9537c7d3d0f84e7): Script killed by user with SCRIPT KILL...
(5.00s)

錯誤處理

redis.call()redis.pcall() 的惟一區別在於它們對錯誤處理的不一樣.

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.call() 不一樣, 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

帶寬 和 EVALSHA

EVAL 命令要求你在每次執行腳本的時候都發送一次腳本主體(script body).

Redis 有一個內部的緩存機制, 所以它不會每次都從新編譯腳本, 不過在不少場合, 付出無謂的帶寬來傳送腳本主體並非最佳選擇.

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

EVALSHA 命令的表現以下:

  • 若是服務器還記得給定的 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).
值得注意的是:
可使用 EVALSHA 來代替 EVAL, 當出現 NOSCRIPT 錯誤時, 才使用 EVAL 命令從新發送腳本, 這樣就能夠最大限度地節省帶寬.

執行 EVAL 命令時, 要使用正確的格式來傳遞鍵名參數和附加參數, 由於若是將參數硬寫在腳本中, 那麼每次當參數改變的時候, 都要從新發送腳本, 即便腳本的主體並無改變.
相反, 經過使用正確的格式來傳遞鍵名參數和附加參數, 就能夠在腳本主體不變的狀況下, 直接使用 EVALSHA 命令對腳本進行復用, 免去了無謂的帶寬消耗.

全局變量保護

爲了防止沒必要要的數據泄漏進 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'

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

一旦用戶在腳本中混入了 Lua 全局狀態, 那麼 AOF 持久化和複製 (replication) 都會沒法保證, 因此, 請不要使用全局變量.

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

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

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

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

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

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

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

  • Redis 記錄一個腳本正在超時運行.
  • Redis 開始從新接受其餘客戶端的命令請求, 可是隻有 SCRIPT KILLSHUTDOWN NOSAVE 兩個命令會被處理, 對於其餘命令請求, Redis 服務器只是簡單地返回 BUSY 錯誤.
  • 可使用 SCRIPT KILL 命令將一個僅執行只讀命令的腳本殺死, 由於只讀命令並不修改數據, 所以殺死這個腳本並不破壞數據的完整性.
  • 若是腳本已經執行過寫命令, 那麼惟一容許執行的操做就是 SHUTDOWN NOSAVE, 它經過中止服務器來阻止當前數據集寫入磁盤.

流水線 (pipeline) 上下文 (context) 中的 EVALSHA

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

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

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

  • 老是在流水線中使用 EVAL 命令.
  • 檢查流水線中要用到的全部命令, 找到其中的 EVAL 命令, 並使用 SCRIPT EXISTS sha1 [sha1 …] 命令檢查要用到的腳本是否是全都已經保存在緩存裏面了. 若是所需的所有腳本均可以在緩存裏找到, 那麼就能夠放心地將全部 EVAL 命令改爲 EVALSHA 命令, 不然的話, 就要在流水線的頂端 (top) 將缺乏的腳本用 SCRIPT LOAD script 命令加上去.
相關文章
相關標籤/搜索