• Redis中的Lua脚本(一)


    Lua脚本

    概述

    Redis从2.6版本开始引入对Lua脚本的支持,通过在服务器中嵌入Lua环境,Redis客户端可以使用Lua脚本,直接在服务器端原子地执行多个Redis命令。其中使用EVAL命令可以直接对输入的脚本进行求值:

    127.0.0.1:6379> EVAL "return 'hello world'" 0
    "hello world"
    
    • 1
    • 2

    而使用EVALSHA命令则可以根据脚本的SHA1校验和来对脚本进行求值,但这个命令要求校验和对应的脚本必须至少被EVAL命令执行过
    一次:

    127.0.0.1:6379> EVAL "return 1+1" 0
    (integer) 2
    127.0.0.1:6379> EVALSHA "a27e7e8a43702b7046d4f6a7ccf5b60cef6b9bd9" 0 // 上一个脚本的校验和
    (integer) 2
    
    • 1
    • 2
    • 3
    • 4

    或者这个校验和对应的脚本曾经被SCRIPT LOAD命令载入过:

    127.0.0.1:6379> SCRIPT Load "return 2*2"
    "4475bfb5919b5ad16424cb50f74d4724ae833e72"
    127.0.0.1:6379> EVALSHA "4475bfb5919b5ad16424cb50f74d4724ae833e72" 0
    (integer) 4
    
    • 1
    • 2
    • 3
    • 4

    创建并修改Lua环境

    为了在Redis服务器中执行Lua脚本,Redis在服务器内嵌了一个Lua环境(evnironment),并对这个Lua环境进行了一系列修改,从而确保这个Lua环境可以满足Redis服务器的需要。Redis服务器创建并修改Lua环境的整个过程由以下步骤组成:

    • 1.创建一个基础的Lua环境,之后的所有修改都是针对这个环境进行的。
    • 2.载入多个函数库到Lua环境里面,让Lua脚本可以使用这些函数库来进行数操作
    • 3.创建全局表格redis,这个表格包含了对Redis进行操作的函数,比如用于在Lua脚本中执行Redis命令的redis.call函数
    • 4.使用Redis自制的随机函数来替换Lua原有的带有副作用的随机函数,从而避免在脚本中引入副作用
    • 5.创建排序辅助函数,Lua环境使用这个辅佐函数来对一部分Redis命令的结果进行排序,从而消除这些命令的不确定性
    • 6.创建redis.pcall函数的错误报告辅助函数,这个函数可以提供更详细的出错信息
    • 7.对Lua环境中的全局环境进行保护,防止用户在执行Lua脚本的过程中,将额外的全局变量添加到Lua环境中
    • 8.将完成修改的Lua环境保存到服务器状态的Lua环境中,等待执行服务器传来的Lua脚本

    创建Lua环境

    服务器首先调用Lua的C API函数lua_open,创建一个新的Lua环境。因为lua_open函数创建的只是一个基本的Lua环境,为了让这个Lua环境可以满足Redis的操作要求,接下来服务器将对这个Lua环境进行一系列修改。

    载入函数库

    Redis修改Lua环境的第一步,就是将以下函数库载入到Lua环境里面:

    • 1.基础库(base library):这个库包含Lua的核心(core)函数,比如assert、error、pairs、tostring、pcall等。另外,为了防止用户从外部文件中引入不安全的代码,库中的loadfile函数会被删除
    • 2.表格库(table library):这个库包含用于处理表格的通用函数。比如table.concat、table.insert、table.remove、table.sort等
    • 3.字符串库(string library):这个库包含用于处理字符串的通用函数,比如用于对字符串进行查找的string.find函数,对字符串进行格式化的string.format函数,查看字符串长度的string.len函数,对字符串进行反转的string.reverse函数等
    • 4.数据库(math libraray):这个库是标准C语言数据库的结构,它包括计算绝对值的math.abs函数,返回多个数中的最大值和最小值的math.max函数和math.min函数,计算二次方根的math.sqrt函数,计算对数的math.log函数等
    • 5.调试库(debug libraray):这个库提供了对程序进行调试所需的函数,比如对程序设置钩子和取得钩子的debug.sethook函数和debug.gethook函数,返回给定函数相关信息的debug.getinfo函数,为对象设置元数据的debug.setmetatable函数,获取对象元数据的debug.getmetatable函数等
    • 6.Lua CJSON库:这个库用于处理UTF-8编码的JSON格式,其中cjson.decode函数将一个JSON格式的字符串转换为一个Lua值,
      而cjson.encode函数将一个Lua值序列化为JSON格式的字符串
    • 7.Struct库:这个库用于在Lua值和C结构(struct)之间进行转换,函数struct.pack将多个Lua值打包成一个类结构(struct-like)字符串,而函数struct.unpack则从一个类结构字符串中解包出多个Lua值
    • 8.Lua cmsgpack库:这个库用于处理MessagePack格式的数据,其中cmsgpack.pack函数将Lua值转换为MessagePack数据,而cmsgpack.unpack函数则将messagepack数据转换为Lua值通过使用这些功能强大的函数库,Lua脚本可以直接对执行Redis命令获得的数据进行复杂的操作

    创建Redis全局

    服务器将在Lua环境中创建一个redis表格(table),并将它设置为全局变量,这个redis表格包含以下函数:

    • 1.用于执行Redis命令的redis.call和redis.pcall函数
    • 2.用于记录Redis日志(log)的redis.log函数,以及相应的日志级别(level)常量:redis.LOG_DEBUG,redis.LOG_VERBOSE,redis.LOG_NOTICE以及redis.LOG_WARNING
    • 3.用于计算SHA1校验和的redis.sha1hex函数
    • 4.用于返回错误信息的redis.error_reply函数和redis.status_reply函数。
      在这些函数里面,最常用也最重要的要数redis.call函数和redis.pcall函数,通过这两个函数,用户可以直接在Lua脚本中执行Redis命令:
    127.0.0.1:6379> EVAL "return redis.call('ping')" 0
    PONG
    
    • 1
    • 2

    使用Redis自制的随机函数来替换Lua原有的随机函数

    为了保证相同的脚本可以在不同的机器上产生相同的结果,Redis要求所有传入服务器的Lua脚本,以及Lua环境中的所有函数,都必须是无副作用(side effect)的纯函数(pure function).但是,在之前载入Lua环境的match函数库中,用于生成随机数的math.random函数和math.randomseed函数都是带有副作用的,它们不符合Redis对Lua环境的无副作用要求。因为这个原因,Redis使用自制的函数替换了math库中原有的math.random函数和math.randomseed函数,替换之后的两个函数有以下特征:

    • 1.对于相同的seed来说,math.random总产生相同的随机数绪列,这个函数是一个纯函数
    • 2.除非在脚本中使用math.randomseed显示地修改seed,否则每次运行脚本时,Lua环境都使用固定地math.randomseed(0)语句来初始化seed
    例子
    • 举个例子。使用以下脚本,我们可以打印seed值为0时,math.random对于输入10至1所产生地随机绪列
    --random-with-default-seed.lua
    
    local i = 10
    local seq ={}
    while (i > 0) do
    seq[i] = math.random(i)
    i = i+1
    end
    
    return seq
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    无论执行这个脚本多少次产生的值都是相同的

    E:\redis>redis-cli --eval test.lua
    1) (integer) 1
    2) (integer) 2
    3) (integer) 2
    4) (integer) 3
    5) (integer) 4
    6) (integer) 4
    7) (integer) 7
    8) (integer) 1
    9) (integer) 7
    10) (integer) 2
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    但是如果我们在另一个脚本里面调用math.randomseed将seed修改为10086

    --random-with-default-seed.lua
    
    math.randomseed(10086) -- change seed
    local i = 10
    local seq ={}
    while (i > 0) do
    seq[i] = math.random(i)
    i = i-1
    end
    
    return seq
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    那么这个脚本生成的随机数绪列将和使用默认seed值0时生成的随机绪列不同:

    E:\redis>redis-cli --eval test1.lua
    1) (integer) 1
    2) (integer) 1
    3) (integer) 2
    4) (integer) 1
    5) (integer) 1
    6) (integer) 3
    7) (integer) 1
    8) (integer) 1
    9) (integer) 3
    10) (integer) 1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
  • 相关阅读:
    java157-线程的引入
    Kafka事务「原理剖析」
    JVM分析指令解析-jps/jinfo/jstat/jstack/jmap
    【Rust日报】2022-09-09 攻击 Firecracker
    【SpringBoot定时任务篇】-----Quartz以及Task详解
    基于Qt 多线程(继承 QObject 的线程)
    写给Python社群的第5课:Python 函数,真的难吗?
    一张VR图像帧的生命周期
    5.区块链系列之私钥管理
    对于使用win32 API获取性能计数器的理解
  • 原文地址:https://blog.csdn.net/Cover_sky/article/details/137892878