当我要执行一组 redis 指令,减低通讯的成本的时候可以用lua。
但是pipeline这个命令也是可以用的,那为什么不用pipeline呢?
首先说下pipeline的局限,如果我想在管道里面做运算(线程安全)的操作是不能做到的。
如: 卖商品,当库存到0的时候,就不需要再减了。不然就会变负数了。
然后就是 pipeline,如果 A命令要依赖于B的结果,这样也获取不到。
上面这种场景pipeline是做不到的,这个时候就需要用到Redis的lua脚本了。
EVAL 简介
EVAL 和 EVALSHA 从 Redis 2.6 版本开始,使用内置的 Lua 解析器,可以对 Lua进行求值。
EVAL script numkeys key [key ...] arg [arg ...]
script :lua 脚本
numkeys :key 的数量
key :key列表,可以在 Lua 中通过全局变量 KEYS 数组,以 1 为 基址的形式访问 (KEYS[1],KEYS[2] ...)
arg :参数列表,可以在 Lua 中通过全局变量 ARGV 数组,以 1 为 基址的形式访问 (ARGV [1],ARGV [2] ...)
eval "return redis.call('set',KEYS[1],'bar')" 1 foo
如何从 Lua 脚如何调用 Redis 命令?
redis.call() :将返回给调用者一个错误
redis.pcall() :将捕获的错误以Lua表的形式返回
创建并修改Lua环境
Redis为了在Redis服务器中执行 Lua 脚本,Redis在服务器内嵌了一个Lua环境,并对这个 Lua环境进行,并对这个 Lua环境进行一系列修改,从而确保这个Lua环境考验满足 Redis 服务器的需要。
创建一个基础的 Lua 环境,之后所有修改都是针对这个环境进行的。
载入多个函数库到 Lua 环境
创建全局表格 reids,这个表格包括对 Redis 进行操作的函数
创建随机函数 random-with-default-seed.lua
创建排序辅助函数
创建 redis.pcall 函数的错误报告
1.创建 Lua 环境
服务器首先调用 Lua 的 C API函数 lua_open,创建一个新的 Lua 环境。
2.载入函数库
基础库 (base) :如 assert、error、pairs、tostring、pcall 等
表格库(table):如 table.concat、table.insert、table.remove、table.sort 等
字符串库(string):处理字符串的通用函数 string.format 、string.len、string.reveres 函数
数学库(math):math.abs、math.max、math.min、math.sqrt、math.log
调用库(debug):提供程序设置钩子 debug.sethook 和 取得钩子 debug.gethook 函数
Lua CJSON库:用于处理 UTF-8 编码的 JSON 格式。 cjson.decode 函数将一个 JSON 格式的字符串转换为一个 Lua值,而 cjson.encode 函数将一个 Lua 值序列化为 JSON 格式字符串。
Struct库:用于 Lua 值和C结果之间转换
Lua cmsgpack库:用于处理 MessagePack 格式数据
3.创建 redis 全局表格
服务器将在 Lua 环境创建一个 reids 表格(table),并将它设为全局变量。reids 表格包括以下函数:
用于执行 Redis 命令的 redis.call 和 redis.pcall 函数。(常用)
用于记录 Redis 日志(log) 的 redis.log 函数,以及相应的日志级别 (level) 处理
用于计算 SHA1 校验和的 redis.sha1hex 函数
用于返回错误信息的 redis.error_reply 函数和 redis.status_reply 函数。
4.伪客户端
因为执行 Redis 命令必须有相应的 客户端状态,所以为了执行 Lua 脚本中包括的命令。Redis 服务器专门为 Lua环境创建了一个伪客户端,并由一个伪客户端处理 Lua脚本中的所有 Redis命令。
EVAL "return redis.call(‘DSSIZE’)" 0
lua_scripts 字典
除了伪客户端之外,Redis 服务器为 Lua 环境创建的另外一个主键就是 lua_scripts,这个字典的键为某个Lua 脚本的 SHA1 校验和 (checksum),而字典的值则是 SHA1校验和 lua脚本:
struct redisServer{
// ...
dict *lua_scripts;
// ...
Redis 服务器会将所有的被 EVAL 命令执行过的Lua 脚本,以及所有被 SCRIPT LOAD 命令载入过的 Lua 脚本都保存到 lua_scripts 字典里。
r127.0.0.1:6371> SCRIPT LOAD "return 'hi'"
"2f31ba2bb6d6a0f42cc159d2e2dad55440778de3"
127.0.0.1:6371> SCRIPT LOAD "return 1+1"
"a27e7e8a43702b7046d4f6a7ccf5b60cef6b9bd9"
127.0.0.1:6371> SCRIPT LOAD "return 2*2"
"4475bfb5919b5ad16424cb50f74d4724ae833e72"
lua_scripts 字典有两个作用,一个是实现 SCRIPT EXISTS 命令,另外一个实现脚本复制功能。
EVAL命令的实现
EVAL 命令的执行过程可以分为以下三个步骤:
根据客户端给定的 Lua 脚本,在 Lua 环境中定义一个 Lua函数
将客户端给定的脚本保存到 lua_scripts 字典,等待将来进一步使用
执行刚刚在 Lua 环境定义的函数,以此来执行客户端执行的 Lua 脚本。
127.0.0.1:6371> EVAL "return 'hello world'" 0
"hello world"
定义脚本函数
当客户端向服务器发送 EVAL 命令,要求执行某个 Lua脚本,服务器首先要做的就是在 Lua环境中为传入的脚本定义一个与这个脚本相对应的 Lua函数,其中,Lua函数的名字是由 f_ 前缀加上脚本的 SHA1 校验和 (四十字字符长) 组成,而函数的体 (body) 则是脚本本身。
我们输入 :EVAL "return 'hello world'" 0
对于服务器来说:
function f_5332031c6b470dc5a0dd9b4bf2030dea6d65de91()
return "hello world"
因为客户端传入脚本为 return 'hello world',而这个脚本的 SHA1 校验和为 5332031c6b470dc5a0dd9b4bf2030dea6d65de91,所以函数名字为 f_5332031c6b470dc5a0dd9b4bf2030dea6d65de91 ,而函数体则为 return "hello world"。
使用函数来保存客户端传入脚本有以下好处:
执行脚本的步骤非常简单,只要调用脚本相对应的函数即可。
通过函数局部性来让 Lua 环境保存清洁,减少垃圾回收的工作量,并且避免了使用全局变量
如果某个脚本所对应的在 Lua环境中被定义过至少一次,那么只要记得调用这个脚本的 SHA1 校验和,服务器就可以在不知道脚本本身直接调用。
将脚本保存到 lua_script 字典
EVAL命令第二件事就是将客户端传入的脚步保存到服务器的 lua_script 字典里面。
return 'hello world'
=====>>>
5332031c6b470dc5a0dd9b4bf2030dea6d65de91
执行脚本函数
当把脚本保存到 lua_script 字典后,服务器还要设置钩子,传参,才能正式执行脚本。
流程如下:
将EVAL 命令传入的键名和参数分别保存到 KEY 数组和 ARGV数组,然后把两个数组作为
全局变量传入 Lua 环境里面。
为 Lua环境装载超时处理钩子(hook),这个钩子可以在脚本出现超时运行情况时,让客户端通过
SCRIPT KILL 命令停止脚本,或者通过 SHUTDOWN 命令直接关闭服务器。
执行脚本函数
移除之前装载的超时钩子
将执行脚本函数所得的结果保存到客户端状态的缓冲区里面,等等服务器将结果返回到客户端。
对 Lua 环境执行垃圾回收操作。
EVALSHA 命令的实现
每个被EVAL命令的成功执行过的 Lua脚本,在 Lua环境里面都有一个脚本相对应的 Lua函数。
注意这个返回错误 “SCRIPRT NOT FOUND”
SCRIPT LOAD
SCRIPT LOAD 先在 Lua环境创建函数,然后再把脚本保存到 lua_scripts 字典里面。
SCRIPT KILL
如果服务器设置了 lua-time-limit 配置,每次执行 Lua脚本直接,服务器都会在 Lua环境里面设置一个超时处理钩子 (hook)。
主从服复制脚本
主服务器复制 EVAL、SCRIPT FLUSH、SCRIPT LOAD 三个命令和复制普通Redis命令一样,只要将相同的命令传播给从服就可以了。
Redisson 实现的客户端有 Lua脚本进行了什么优化?
参数 : useScriptCache
Default value: false
定义是否在Redis端使用Lua脚本缓存。大多数Redisson方法都是基于Lua脚本的,
打开此设置可以提高此类方法的执行速度并节省网络流量。
先把命令转换成 sha1码
然后执行一遍 “EVALSHA” 命令,监听命令返回命令
返回 “NOSCRIPT” 就是证明该 Redis服务端没有这个脚本了,执行一遍 “SCRIPT_LOAD” 命令,把脚本load进去,继续监听 “SCRIPT_LOAD” 命令的返回
“SCRIPT_LOAD”命令返回成功就再发一次 “EVALSHA” 命令
代码如下:
private <T, R> RFuture<R> evalAsync(NodeSource nodeSource, boolean readOnlyMode, Codec codec, RedisCommand<T> evalCommandType, String script, List<Object> keys, Object... params) {
// 如果 useScriptCache == true 并且是 脚本命令 EVAL
if (isEvalCacheActive() && evalCommandType.getName().equals("EVAL")) {
RPromise<R> mainPromise = new RedissonPromise<R>()
Object[] pps = copy(params)
RPromise<R> promise = new RedissonPromise<R>()
// 先把命令转成 sha1码
String sha1 = calcSHA(script)
RedisCommand cmd = new RedisCommand(evalCommandType, "EVALSHA")
List<Object> args = new ArrayList<Object>(2 + keys.size() + params.length)
args.add(sha1)
args.add(keys.size())
args.addAll(keys)
args.addAll(Arrays.asList(params))
// 发送 EVALSHA 命令
RedisExecutor<T, R> executor = new RedisExecutor<>(readOnlyMode, nodeSource, codec, cmd,
args.toArray(), promise, false, connectionManager, objectBuilder, referenceType)
executor.execute()
// 监听返回
promise.onComplete((res, e) -> {
if (e != null) {
// redis 服务端返回 “NOSCRIPT” 证明 Redis 没有这个脚本
if (e.getMessage().startsWith("NOSCRIPT")) {
// 执行 “SCRIPT_LOAD” 命令先把脚本加载到 Redis服务端
RFuture<String> loadFuture = loadScript(executor.getRedisClient(), script)
// 监听返回
loadFuture.onComplete((r, ex) -> {
if (ex != null) {
free(pps)
mainPromise.tryFailure(ex)
return
// “SCRIPT_LOAD” 执行成功,再一次执行 “EVALSHA”
RedisCommand command = new RedisCommand(evalCommandType, "EVALSHA")
List<Object> newargs = new ArrayList<Object>(2 + keys.size() + params.length)
newargs.add(sha1)
newargs.add(keys.size())
newargs.addAll(keys)
newargs.addAll(Arrays.asList(pps))
NodeSource ns = nodeSource
if (ns.getRedisClient() == null) {
ns = new NodeSource(nodeSource, executor.getRedisClient())
async(readOnlyMode, ns, codec, command, newargs.toArray(), mainPromise, false)
} else {
free(pps)
mainPromise.tryFailure(e)
return
free(pps)
mainPromise.trySuccess(res)
return mainPromise
// 没有做优化,正常发送请求
RPromise<R> mainPromise = createPromise()
List<Object> args = new ArrayList<Object>(2 + keys.size() + params.length)
args.add(script)
args.add(keys.size())
args.addAll(keys)
args.addAll(Arrays.asList(params))
async(readOnlyMode, nodeSource, codec, evalCommandType, args.toArray(), mainPromise, false)
return mainPromise
redis.cn/commands/ev… 官方文档
www.cnblogs.com/chopper-poe… 参考