1. 程式人生 > 程式設計 >Node.js 中實踐 Redis Lua 指令碼

Node.js 中實踐 Redis Lua 指令碼

對別人的意見要表示尊重。千萬別說:"你錯了。"——卡耐基

Lua 是一種輕量小巧的指令碼語言,用標準 C 語言編寫並以原始碼形式開放,其設計目的是為了嵌入應用程式中,從而為應用程式提供靈活的擴充套件和定製功能。由於 Lua 語言具備原子性,其在執行的過程中不會被其它程式打斷,對於併發下資料的一致性是有幫助的。

作者簡介:五月君,Nodejs Developer,網認證作者,熱愛技術、喜歡分享的 90 後青年,歡迎關注 Nodejs技術棧 和 Github 開源專案 www.nodejs.red

Redis 的兩種 Lua 指令碼

Redis 支援兩種執行 Lua 指令碼的方式,一種是直接在 Redis 中輸入 Lua 程式碼,適合於一些簡單的指令碼。另一種方式是編寫 Lua 指令碼檔案,適合於有邏輯運算的情況,Redis 使用 SHA1 演演算法支援對指令碼簽名和 Script Load 預先快取,需要執行的時候通過簽名返回的識別符號即可。

下面會分別介紹如何應用 Redis 提供的 EVAL、EVALSHA 兩個命令來實現對 Lua 指令碼的應用,同時介紹一些在 Node.js 中該如何去應用 Redis 的 Lua 指令碼。

EVAL

Redis 2.6.0 版本開始,通過內建的 Lua 直譯器,可以使用 EVAL 命令對 Lua 指令碼進行求值

  • script:執行的指令碼
  • numkeys:指定鍵名引數個數
  • key:鍵名,可以多個(key1、key2),通過 KEYS[1] KEYS[2] 的形式訪問
  • atg:鍵值,可以多個(val1、val2),通過 ARGS[1] ARGS[2] 的形式訪問
EVAL script numkeys key [key ...] arg
[arg ...] 複製程式碼

EVAL Redis 控制檯實踐

按照上面命令格式,寫一個例項如下,通過 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)

127.0.0.1:6379> EVAL "redis.call('SET',ARGV[2])" 2 name1 name2 val1 val2
(nil)
複製程式碼

redis.call VS redis.pcall

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_',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_',ARGV[2]); redis.pcall('SET',ARGV[3])" 2 name1 name2 val1 val2 val3
(nil)
複製程式碼

EVAL 在 Node.js 中實現

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',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();
複製程式碼

EVALSHA

EVAL 命令要求你在每次執行指令碼的時候都傳送一次指令碼主體 (script body)。Redis 有一個內部的快取機制,因此它不會每次都重新編譯指令碼,通過 EVALSHA 來實現,根據給定的 SHA1 校驗碼,對快取在伺服器中的指令碼進行求值。SHA1 怎麼生成呢?通過 script 命令,可以對指令碼快取進行操作

  • SCRIPT FLUSH:清除所有指令碼快取
  • SCRIPT EXISTS:檢查指定的指令碼是否存在於指令碼快取
  • SCRIPT LOAD:將一個指令碼裝入指令碼快取,但並不立即執行它
  • SCRIPT KILL:殺死當前正在執行的指令碼

EVALSHA 命令格式

同上面 EVAL 不同的是前面 EVAL script 換成了 EVALSHA sha1

EVALSHA sha1 numkeys key [key ...] arg [arg ...]
複製程式碼

EVALSHA Redis 控制檯實踐

載入指令碼快取

127.0.0.1:6379> SCRIPT LOAD "redis.pcall('SET',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"
複製程式碼

EVALSHA 在 Node.js 中實現

分為三步:快取指令碼、執行指令碼、獲取資料

const Redis = require("ioredis");
const redis = new Redis(6379,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','val2');

    // 3. 獲取資料
    const result = await redis.get("name1");
    console.log(result); // "val2"
}

evalSHA();
複製程式碼

Lua 指令碼檔案

有邏輯運算的指令碼,可以編寫 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,20,10);
    const result2 = await redis.eval(redisLuaScript,10,20);
    console.log(result1,result2); // 1 0
}

test();
複製程式碼