一, 前言
运行环境 Windows,Redis 版本 3.2.1. 此处暂不对 Lua 进行详细讲解, 只从 Redis 的方面讲解.
二, Redis 的 Lua 脚本
在 Redis 的 2.6 版本推出了脚本功能, 允许开发者使用 Lua 语言编写脚本传到 Redis 中执行, 在 Lua 脚本中也可以调用大部分的 Redis 命令. 使用脚本有以下三个好处:
(1) 减少网络开销: 有些时候需要多次请求 Redis 获取处理数据, 而使用脚本功能就可以只使用一次请求完成相同操作, 减少了网络往返时延.
(2) 原子操作: Redis 会将整个脚本作为一个整体执行, 中间不会被其他命令插入. 也就是说在编写脚本的过程中无须担心会出现竞态条件, 也就是无须使用事务. 事务可以完成的所有功能, 都可以用脚本来完成.
(3) 复用: 客户端发送的脚本会永久存储在 Redis 中, 这就意味着其他客户端 (可以是其他语言开发的项目) 可以复用这一脚本而不需要使用代码完成同样的逻辑.
三, Redis 调用 Lua
1,EVAL 命令
编写完脚本后最重要的就是在程序中执行脚本. Redis 提供了 EVAL 命令可以使开发者像调用其他 Redis 内置命令一样调用脚本. EVAL 的命令格式如下:
127.0.0.1:6379> eval script numkeys key [key ...] arg [arg ...]
script: 脚本内容. numkeys:key 参数的数量. key 和 arg: 这两个参数向脚本传递数据, 它们的值可以在脚本中分别使用 KEYS[index]和 ARGV[index]两个表类型的全局变量访问, numkeys 为 key 的数量和其索引的最大值, argv 的索引为 key 和 argv 数量总和减去 numkeys, 它们的索引都是从 1 开始, 超出则返回 nil. 如下:
- C:\Users\Xu>Redis-cli
- 127.0.0.1:6379> eval 'return ARGV[3]' 2 key1 key2 value1 value2 value3
- "value3"
- 127.0.0.1:6379> eval 'return KEYS[2]' 2 key1 key2 value1 value2 value3
- "key2"
- 127.0.0.1:6379> eval 'return KEYS[3]' 2 key1 key2 value1 value2 value3
- (nil)
其中要读写的键名应该为 key 参数, 其他数据都作为 arg 参数.
除了上面直接写 lua 脚本, 还可以读取 lua 脚本文件来执行脚本, 命令如下:
C:\Users\Xu>Redis-cli --eval lua_file_path key1 key2 , arg1 arg2 arg3
注意不需要 numkeys, 逗号前后必须有空格, 否则会被认为一个连起来的字符串.
- //lua 文件内容
- return ARGV[2]
- // 执行命令
- C:\Users\Xu>Redis-cli.exe --eval e:\Redis\a.lua key1 , value1 value2
- "value2"
- C:\Users\Xu>Redis-cli.exe --eval e:\Redis\a.lua key1 , value1 value2,value3
- "value2,value3"
2,EVALSHA 命令
考虑到在脚本比较长的时候, 如果每次调用脚本都需要将整个脚本传给 Redis 会占用较多的带宽. 所以, Redis 提供了 EVALSHA 命令允许开发者通过脚本内容的 SHA1 摘要来执行脚本, 该命令的用法和 EVAL 一样, 不过就是将脚本内容的 script 替换为它的 SHA1 摘要.
Redis 在执行 EVAL 命令时会计算脚本的 SHA1 摘要并记录在脚本缓存中, 如果执行 EVALSHA 命令时没有从脚本缓存中找到相应的摘要, 则返回错误.
- 127.0.0.1:6379> evalsha c349a436bd639369c62c971941fc5f7a80626ee6 1 key1 value1
- (integer) 666
- 127.0.0.1:6379> evalsha c349a436bd639369c62c971941fc5f7a80626ee61 1 key1 value1
- (error) NOSCRIPT No matching script. Please use EVAL.
在程序中使用 EVALSHA 的流程如下:
(1) 先计算脚本 SHA1 摘要, 并使用 EVALSHA 执行.
(2) 获得返回值, 如果返回错误则使用 EVAL 重新执行脚本.
3,SCRIPT LOAD 命令
如果只是想将脚本加入到脚本缓存中而不执行则则可以用 SCRIPT LOAD 命令, 返回值时脚本的 SHA1 摘要.
- 127.0.0.1:6379> script load 'return 666'
- "c349a436bd639369c62c971941fc5f7a80626ee6"
4,SCRIPT EXISTS 命令
SCRIPT EXISTS 命令可以同时查找一个或者多个脚本的 SHA1 摘要是否已经本缓存, 1 为存在 0 为不存在.
- 127.0.0.1:6379> script exists c349a436bd639369c62c971941fc5f7a80626ee6 123ls436bd639369c62c971941fc5f7a80626ee6
- 1) (integer) 1
- 2) (integer) 0
5,SCRIPT FLUSH 命令
Redis 将脚本的 SHA1 摘要加入到脚本缓存后会永久保存, 不会删除, 但是可以用 SCRIPT FLUSH 删除所有脚本缓存.
- 127.0.0.1:6379> script flush
- OK
- (1.51s)
6,SCRIPT KILL 和 SHUTDOWN NOSAVE
由于 Redis 的脚本是原子性的, 脚本执行期间不会执行其他命令. 为了防止某个脚本执行时间过长导致 Redis 无法提供服务(比如死循环),Redis 提供了 lua-time-limit 参数限制脚本最长运行时间, 默认是 5 秒. 再脚本执行期间, 执行其他命令会返回 "BUSY" 错误, 如下:
- (A)127.0.0.1:6379> eval 'while true do end' 0
- (B)127.0.0.1:6379> get foo
- (error) BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.
此时 Redis 只会接受并执行两个命令: SCRIPT KILL 和 SHUTDOWN NOSVAE.
通过 SCRIPT KILL 可以终止当前脚本的运行, 脚本停止并返回错误:
- (B)127.0.0.1:6379> script kill
- OK
- (B)127.0.0.1:6379> get foo
- (nil)
- (A)127.0.0.1:6379> eval 'while true do end' 0
- (error) ERR Error running script (call to f_694a5fe1ddb97a4c6a1bf299d9537c7d3d0f84e7): @user_script:1: Script killed by user with SCRIPT KILL...
- (175.99s)
如果当前执行的脚本对 Redis 的数据进行了修改, 则 SCRIPT KILL 不会终止脚本的运行, 因为这样违背了原子性. 那么需要通过 SHUTDOWN NOSAVE 来强制终止 Redis 将原先脚本的修改操作返回, 不进行持久化操作, 这意味着所有发送在上一次的快照后的数据库修改都会丢失.
四, Redis 获取脚本中的返回值
很多情况下, 都需要脚本通过 return 返回值, 如果没有执行 return 则默认返回 nil. 因为我们可以像调用其他 Redis 内置命令一样调用我们自己写的脚本, 所以同样 Redis 会自动将脚本返回值的 Lua 数据类型转化成 Redis 的返回值类型. 具体的转换规则如下:
(1) Lua 的数字类型, Redis 为整数类型.
- 127.0.0.1:6379> eval 'return 1.1' 0
- (integer) 1
(2) Lua 的字符串类型, Redis 也是字符串类型
(3) Lua 的表类型(数组形式),Redis 会返回多行字符串
- 127.0.0.1:6379> eval 'return {0,1}' 0
- 1) (integer) 0
- 2) (integer) 1
(4) Lua 表类型(只有一个 ok 字段存储状态信息),Redis 为成功状态回复
- 127.0.0.1:6379> eval 'return {ok="this is ok"}' 0
- this is ok
(5)Lua 表类型(只有一个 err 字段存储状态信息),Redis 为错误状态回复
- 127.0.0.1:6379> eval 'return {err="so bad"}' 0
- (error) so bad
(6)Lua 的 bool 类型中 true 为 Redis 的 1,false 为 nil
- 127.0.0.1:6379> eval 'return true' 0
- (integer) 1
- 127.0.0.1:6379> eval 'return false' 0
- (nil)
五, 沙盒与随机数
Redis 脚本禁止使用 Lua 标准库中与文件或系统调用相关的函数, 在脚本中只允许对 Redis 的数据进行处理. 并且 Redis 还通过禁用脚本的全局变量的方式保证每个脚本都是相对隔离的, 不会互相干扰.
使用沙盒不仅是为了保证服务器的安全性, 而且还确保了脚本的执行结果只有和脚本本身和执行时传递的参数有关, 不依赖外界条件(如系统时间, 系统中某个文件的内容, 其他脚本执行结果登). 这是因为在执行复制和 AOF 持久话操作时记录的脚本的内容而不是脚本调用的命令, 所以必须保证在脚本内容和参数一样的前提下脚本的执行结果必须一样.
对于随机数, Redis 替换了 math.random 和 math.randomseed 函数使得每次执行脚本时生成的随机数列都相同, 如果希望获得不同的随机数序列, 最简单的方法时由程序生成随机数并通过参数传递给脚本, 或者采用更灵活的方法, 即在程序中生成随机数传给脚本作为随机数种子.
六, 在 net core 中使用脚本
很简单, 直接上代码, 这里举例最基本的, 还有很多的重写方法大家可以自己试试. 最简单的使用 eval.
- var script = "return KEYS[1];";
- var keys = new RedisKey[]{
- "key1","key2"
- };
- var values = new RedisValue[] {
- "value1", "value2"
- };
- return await redisConnection.GetDatabase().ScriptEvaluateAsync(script, keys, values);
缓存脚本, 并使用.
- var bytes = await redisConnection.GetServer(Config.Get("ConnectionStrings:Redis:ConnectionString")).ScriptLoadAsync("return 1");
- var result = await redisConnection.GetDatabase().ScriptEvaluateAsync(bytes, null, null);
脚本是否已缓存.
bool exist = await redisConnection.GetServer(Config.Get("ConnectionStrings:Redis:ConnectionString")).ScriptExistsAsync("return 1");
删除所有脚本缓存, 这个操作需要连接的 ConfigurationOptions 配置中 AllowAdmin = true, 没有会报错哦.
redisConnection.GetServer(Config.Get("ConnectionStrings:Redis:ConnectionString")).ScriptFlush();
还有 LuaScript 和 LoadedLuaScript 两个类可以对脚本进行更多复杂的脚本, LuaScript 将 @myVar 形式的脚本中的变量重写为 Redis 所需的合适的 ARGV[someIndex]. 如果传递的参数是 RedisKey 类型, 它将作为 KEYS 集合的一部分自动发送. 如下.
- var lua = LuaScript.Prepare("return @key");
- var result = redisConnection.GetDatabase().ScriptEvaluate(lua,new {
- key= (RedisKey)"key1",value = "value1"
- });
来源: https://www.cnblogs.com/xwc1996/p/12188963.html