對別人的意見要表示尊重。千萬別說:"你錯了。"——卡耐基html
Lua 是一種輕量小巧的腳本語言,用標準 C 語言編寫並以源代碼形式開放,其設計目的是爲了嵌入應用程序中,從而爲應用程序提供靈活的擴展和定製功能。因爲 Lua 語言具有原子性,其在執行的過程當中不會被其它程序打斷,對於併發下數據的一致性是有幫助的。node
做者簡介:五月君,Nodejs Developer,慕課網認證做者,熱愛技術、喜歡分享的 90 後青年,歡迎關注 Nodejs技術棧 和 Github 開源項目 www.nodejs.redgit
Redis 支持兩種運行 Lua 腳本的方式,一種是直接在 Redis 中輸入 Lua 代碼,適合於一些簡單的腳本。另外一種方式是編寫 Lua 腳本文件,適合於有邏輯運算的狀況,Redis 使用 SHA1 算法支持對腳本簽名和 Script Load 預先緩存,須要運行的時候經過簽名返回的標識符便可。github
下面會分別介紹如何應用 Redis 提供的 EVAL、EVALSHA 兩個命令來實現對 Lua 腳本的應用,同時介紹一些在 Node.js 中該如何去應用 Redis 的 Lua 腳本。redis
Redis 2.6.0 版本開始,經過內置的 Lua 解釋器,可使用 EVAL 命令對 Lua 腳本進行求值算法
EVAL script numkeys key [key ...] arg [arg ...]
複製代碼
按照上面命令格式,寫一個實例以下,經過 KEYS[] 數組的形式訪問 ARGV[],這裏下標是以 1 開始,KEYS[1] 對應的鍵名爲 name1,ARGV[2] 對應的值爲 val2數組
127.0.0.1:6379> EVAL "return redis.call('SET', KEYS[1], ARGV[2])" 2 name1 name2 val1 val2
OK
複製代碼
執行以上命令,經過 get 查看 name1 對應的值爲 val2緩存
127.0.0.1:6379> get name1
"val2"
複製代碼
注意:以上命令若是不使用 return 將會返回 (nil)bash
127.0.0.1:6379> EVAL "redis.call('SET', KEYS[1], ARGV[2])" 2 name1 name2 val1 val2
(nil)
複製代碼
redis.call 和 redis.pcall 是兩個不一樣的 Lua 函數來調用 redis 命令,兩個命令很相似,區別是若是 redis 命令中出現錯誤異常,redis.call 會直接返回一個錯誤信息給調用者,而 redis.pcall 會以 Lua 的形式對錯誤進行捕獲並返回。服務器
使用 redis.call
這裏執行了兩條 Redis 命令,第一條故意寫了一個 SET_ 這是一個錯誤的命令,能夠看到出錯後,錯誤信息被拋出給了調用者,同時你執行 get name2 會獲得 (nil),第二條命令也沒有被執行
127.0.0.1:6379> EVAL "redis.call('SET_', KEYS[1], ARGV[2]); redis.call('SET', KEYS[2], ARGV[3])" 2 name1 name2 val1 val2 val3
(error) ERR Error running script (call to f_bf814e38e3d98242ae0c62791fa299f04e757a7d): @user_script:1: @user_script: 1: Unknown Redis command called from Lua script
複製代碼
使用 redis.pcall
和上面一樣的操做,使用 redis.pcall 能夠看到輸出結果爲 (nil) 它的錯誤被 Lua 捕獲了,這時咱們在執行 get name2 會獲得一個設置好的結果 val3,這裏第二條命令是被執行了的。
EVAL "redis.pcall('SET_', KEYS[1], ARGV[2]); redis.pcall('SET', KEYS[2], ARGV[3])" 2 name1 name2 val1 val2 val3
(nil)
複製代碼
ioredis 支持全部的腳本命令,好比 EVAL、EVALSHA 和 SCRIPT。可是,在現實場景中使用它是很繁瑣的,由於開發人員必須注意腳本緩存,並檢測什麼時候使用 EVAL,什麼時候使用 EVALSHA。ioredis 公開了一個 defineCommand 方法,使腳本更容易使用。
const Redis = require("ioredis");
const redis = new Redis(6379, "127.0.0.1");
const evalScript = `return redis.call('SET', KEYS[1], ARGV[2])`;
redis.defineCommand("evalTest", {
numberOfKeys: 2,
lua: evalScript,
})
async function eval() {
await redis.evalTest('name1', 'name2', 'val1', 'val2');
const result = await redis.get('name1');
console.log(result); // val2
}
eval();
複製代碼
EVAL 命令要求你在每次執行腳本的時候都發送一次腳本主體 (script body)。Redis 有一個內部的緩存機制,所以它不會每次都從新編譯腳本,經過 EVALSHA 來實現,根據給定的 SHA1 校驗碼,對緩存在服務器中的腳本進行求值。SHA1 怎麼生成呢?經過 script 命令,能夠對腳本緩存進行操做
EVALSHA 命令格式
同上面 EVAL 不一樣的是前面 EVAL script 換成了 EVALSHA sha1
EVALSHA sha1 numkeys key [key ...] arg [arg ...]
複製代碼
載入腳本緩存
127.0.0.1:6379> SCRIPT LOAD "redis.pcall('SET', KEYS[1], ARGV[2]);"
"2a3b189808b36be907e26dab7ddcd8428dcd1bc8"
複製代碼
以上腳本執行以後會返回一個 SHA-1 簽名事後的標識字符串,這個字符串用於下面命令執行簽名以後的腳本
127.0.0.1:6379> EVALSHA 2a3b189808b36be907e26dab7ddcd8428dcd1bc8 2 name1 name2 val1 val2
複製代碼
進行 get 操做讀取 name1 的只爲 val2
127.0.0.1:6379> get name1
"val2"
複製代碼
分爲三步:緩存腳本、執行腳本、獲取數據
const Redis = require("ioredis");
const redis = new Redis(6379, "127.0.0.1");
const evalScript = `return redis.call('SET', KEYS[1], ARGV[2])`;
async function evalSHA() {
// 1. 緩存腳本獲取 sha1 值
const sha1 = await redis.script("load", evalScript);
console.log(sha1); // 6bce4ade07396ba3eb2d98e461167563a868c661
// 2. 經過 evalsha 執行腳本
await redis.evalsha(sha1, 2, 'name1', 'name2', 'val1', 'val2');
// 3. 獲取數據
const result = await redis.get("name1");
console.log(result); // "val2"
}
evalSHA();
複製代碼
有邏輯運算的腳本,能夠編寫 Lua 腳本文件,編寫一些簡單的腳本也不難,能夠參考這個教程 www.runoob.com/lua/lua-tut…
Lua 文件
如下是一個測試代碼,經過讀取兩個值比較返回不一樣的值,經過 Lua 腳本實現後能夠多條 Redis 命令的原子性。
-- test.lua
-- 先 SET
redis.call("SET", KEYS[1], ARGV[1])
redis.call("SET", KEYS[2], ARGV[2])
-- GET 取值
local key1 = tonumber(redis.call("GET", KEYS[1]))
local key2 = tonumber(redis.call("GET", KEYS[2]))
-- 若是 key1 小於 key2 返回 0
-- nil 至關於 false
if (key1 == nil or key2 == nil or key1 < key2)
then
return 0
else
return 1
end
複製代碼
Node.js 中加載 Lua 腳本文件
和上面 Node.js 中應用 Lua 差異不大,多了一步,經過 fs 模塊先讀取 Lua 腳本文件,在經過 eval 或者 evalsha 執行。
const Redis = require("ioredis");
const redis = new Redis(6379, "127.0.0.1");
const fs = require('fs');
async function test() {
const redisLuaScript = fs.readFileSync('./test.lua');
const result1 = await redis.eval(redisLuaScript, 2, 'name1', 'name2', 20, 10);
const result2 = await redis.eval(redisLuaScript, 2, 'name1', 'name2', 10, 20);
console.log(result1, result2); // 1 0
}
test();
複製代碼