对别人的意见要表示尊重。千万别说:"你错了。"——卡耐基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();
复制代码