.Net Core使用分布式缓存Redis:Lua脚本
一、前言
运行环境window,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" });