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();
复制代码

相关文章
相关标签/搜索