Node.js 中實踐 Redis Lua 腳本

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

Lua 是一種輕量小巧的腳本語言,用標準 C 語言編寫並以源代碼形式開放,其設計目的是爲了嵌入應用程序中,從而爲應用程序提供靈活的擴展和定製功能。因爲 Lua 語言具有原子性,其在執行的過程當中不會被其它程序打斷,對於併發下數據的一致性是有幫助的。node

做者簡介:五月君,Nodejs Developer,慕課網認證做者,熱愛技術、喜歡分享的 90 後青年,歡迎關注 Nodejs技術棧 和 Github 開源項目 www.nodejs.redgit

Redis 的兩種 Lua 腳本

Redis 支持兩種運行 Lua 腳本的方式,一種是直接在 Redis 中輸入 Lua 代碼,適合於一些簡單的腳本。另外一種方式是編寫 Lua 腳本文件,適合於有邏輯運算的狀況,Redis 使用 SHA1 算法支持對腳本簽名和 Script Load 預先緩存,須要運行的時候經過簽名返回的標識符便可。github

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

EVAL

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

  • script:執行的腳本
  • numkeys:指定鍵名參數個數
  • key:鍵名,能夠多個(key一、key2),經過 KEYS[1] KEYS[2] 的形式訪問
  • atg:鍵值,能夠多個(val一、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)bash

127.0.0.1:6379> EVAL "redis.call('SET', KEYS[1], 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_', 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)
複製代碼

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

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', 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"
複製代碼

EVALSHA 在 Node.js 中實現

分爲三步:緩存腳本、執行腳本、獲取數據

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 腳本文件

有邏輯運算的腳本,能夠編寫 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();
複製代碼

相關文章
相關標籤/搜索