当前位置: 首页 > 科技观察

一网打尽RedisLua脚本并发原子组合操作

时间:2023-03-15 08:36:52 科技观察

1.前言Redis是一个高性能的KV内存数据库,除了缓存中间件的基本作用外,它还有很多用途。Redis提供了丰富的命令供我们用来实现一些计算。单个Redis命令是原子的,有时我们希望组合多个Redis命令,并使这种组合原子地执行,甚至是可重用的。Redis的开发者意识到这种场景还是很常见的,于是在2.6版本中引入了一个特性来解决这个问题,就是Redis执行Lua脚本。2.LuaLua也被认为是一种古老的语言。玩过魔兽世界的玩家应该都不陌生。WOW的插件是用Lua脚本编写的。Lua在高并发网络游戏中大放异彩,应用广泛。Lua在其他语言中被广泛用作嵌入式脚本,尤其是C/C++。它的语法简单小巧,源代码也只有200多K,这可能是Redis官方选择它的原因。另一个明星软件Nginx也支持Lua,使用Lua可以实现很多有用的功能。3.Lua并不难Redis官方指南也指出不要在Lua脚本中写太复杂的逻辑。为了实现一种功能而学习一门语言,似乎让人望而却步。其实Lua并不难学,而且作为本文的场景,我们不需要学习Lua的全部特性,只是简单的在Redis中使用Lua语言。这对于掌握了Java这种重量级语言的你来说一点都不难。在这里,胖哥只讲Redis涉及的基本语法。Lua的简单语法在Lua的Redis脚本中,我个人建议只需要使用以下类型:nilemptybooleanbooleannumbernumberstringtabletabledeclarationtypedeclarationtype很简单,不需要携带类型。---globalvariablename='felord.cn'---localvariablelocalage=18Redis脚本在实践中不使用全局变量,局部变量效率更高。前四种表类型非常容易理解。第五类表需要简单提一下。它既是数组又类似于Java中的HashMap(字典)。它是Lua中唯一的数据结构。数组不区分具体类型,演示如下Lua5.1.5Copyright(C)1994-2012Lua.org,PUC-Rio>arr_table={'felord.cn','Felordcn',1}>print(arr_table[1])领主。cn>print(arr_table[3])1>print(#arr_table)3作为字典:Lua5.1.5Copyright(C)1994-2012Lua.org,PUC-Rio>arr_table={name='felord.cn',age=18}>print(arr_table['name'])felord.cn>print(arr_table.name)felord.cn>print(arr_table[1])nil>print(arr_table['age'])18>print(#arr_table)0混合模式:Lua5.1.5Copyright(C)1994-2012Lua.org,PUC-Rio>arr_table={'felord.cn','Felordcn',1,age=18,nil}>print(arr_table[1])felord.cn>print(arr_table[4])nil>print(arr_table['age'])18>print(#arr_table)3#表格长度不一定准确,慎用。同时在Redis脚本中避免使用混合模式表,元素中应避免包含空值nil。在不确定元素的情况下,应使用循环来计算实际长度。判断很简单,格式为:locala=10ifa<10thenprint('a小于10')elseifa<20thenprint('a小于20,大于等于10')elseprint('a大于orequalto20')结束数组循环localarr={1,2,name='felord.cn'}fori,vinipairs(arr)doprint('i='..i)print('v='..v)endprint('----------------------')fori,vinpairs(arr)doprint('pi='..i)print('pv='..v)最终打印结果:i=1v=1i=2v=2----------------------pi=1pv=1pi=2pv=2pi=namepv=felord。cn返回值和Python一样,Lua也可以返回多个返回值。但是不建议在Redis的Lua脚本中使用该特性。如果有这样的需求,请封装成数组结构。SpringDataRedis中支持脚本的返回值规则可以从这里分析:publicstaticReturnTypefromJavaType(@NullableClassjavaType){if(javaType==null){returnReturnType.STATUS;}if(javaType.isAssignableFrom(List.class)){returnReturnType.MULTI;}if(javaType.isAssignableFrom(Boolean.class)){returnReturnType.BOOLEAN;}if(javaType.isAssignableFrom(Long.class)){returnReturnType.INTEGER;}returnReturnType.VALUE;}胖哥实战将使用List、Boolean和Long来避免任何问题。至此,RedisLua脚本所需的知识就介绍完了。RedisLua脚本中不应出现其他函数、协程等特性。如果您使用内置函数,只需搜索和查询即可。当你接触到一项新技术时,你首先要经常使用它。想玩花样,就意味着更高的学习成本。4.Redis中的Lua接下来就是RedisLua脚本的实际运行了。EVAL命令EVAL命令在Redis中用于直接执行指定的Lua脚本。EVALluascriptnumkeyskey[key...]arg[arg...]EVAL命令的键。luascriptLua脚本。numkeys指定的lua脚本需要处理key的个数,其实就是key数组的长度。key将零传给多个键给Lua脚本,用空格隔开,在Lua脚本中通过KEYS[INDEX]得到对应的值,其中1<=INDEX<=numkeys。arg是传递给脚本的零个或多个附加参数,以空格分隔,通过Lua脚本中的ARGV[INDEX]获取对应的值,其中1<=INDEX<=numkeys。下面简单演示一下获取密钥hello的简单脚本:127.0.0.1:6379>sethelloworldOK127.0.0.1:6379>gethello"world"127.0.0.1:6379>EVAL"returnredis.call('GET',KEYS[1])"1hello"world"127.0.0.1:6379>EVAL"returnredis.call('GET','hello')"(error)ERRwrongnumberofargumentsfor'eval'command127.0.0.1:6379>EVAL"returnredis.call('GET','hello')"0"world"从上面的demo代码中发现KEYS[1]可以直接换成hello,但是Redis官方文档指出不推荐这样做,目的就是在执行命令之前知道命令被解析,以确保RedisCluster能够将命令转发到合适的集群节点。在任何情况下,numkeys都是必需的命令参数。call函数和pcall函数在上面的例子中,我们通过redis.call()执行了一个SET命令。其实我们也可以用redis.pcall()代替。它们之间的唯一区别是处理错误的方式。前者在执行命令出错时会直接返回错误给调用者;而后者会将错误包装成我们上面提到的表格形式:127.0.0.1:6379>EVAL"returnredis.call('no_command')"0(error)ERRErrorrunningscript(calltof_1e6efd00ab50dd564a9f13e5775e27b966c2141e):@user_script:1:@user_script:1:UnknownRediscommandcalledfromLuascript127.0.0.1:6379>EVAL"0return_r'no)@user_script:1:UnknownRediscommandcalledfromLuascript这就像Java遇到异常,前者会直接抛出异常;后者会把异常处理成JSON并返回.值的转换由于Redis和Lua在Redis中有两种不同的运行环境,所以Redis和Lua在相互传递数据的时候必然会发生相应的转换操作,这个转换操作在实践中是不能忽略的,比如Lua脚本返回一个小数到Redis,则有小数精度损失,转成字符串是安全的127.0.0.1:6379>EVAL"return3.14"0(integer)3127.0.0.1:6379>EVAL"returntostring(3.14)"0《3.14》安全出行ssstringsandintegers根据胖哥的经验,需要去其他人仔细看看官方文档,实际验证一下。原子执行Lua脚本在Redis中以原子方式执行。Redis服务器在执行EVAL命令时,只会执行当前命令指定的Lua脚本中包含的所有逻辑,然后再将结果返回给调用者。其他客户端发送的命令都会被阻塞,直到EVAL命令执行完毕。因此,不建议在LUA脚本中写一些过于复杂的逻辑。需要尽可能保证Lua脚本的效率,否则会影响其他客户端。脚本管理SCRIPTLOAD将脚本加载到缓存中,实现复用,避免多次加载浪费带宽。每个脚本都会通过SHA验证返回一个唯一的字符串标识符。缓存的脚本需要用EVALSHA命令来执行。127.0.0.1:6379>SCRIPTLOAD"return'hello'""1b936e3fe509bcbc9cd0664897bbe8fd0cac101b"127.0.0.1:6379>EVALSHA1b936e3fe509bcbc9cd0664897bbe8fd0cac101b0"hello"SCRIPTFLUSH既然有缓存就有清除缓存,但是遗憾的是并没有根据SHA来删除脚本缓存,而就是清除所有的脚本缓存,所以在制作过程中一般不会用到这个命令。SCRIPTEXISTS检查是否存在一个或多个具有SHA标识符的缓存。127.0.0.1:6379>SCRIPTEXISTS1b936e3fe509bcbc9cd0664897bbe8fd0cac101b1b936e3fe509bcbc9cd0664897bbe8fd0cac10121)(整数)12)(整数)0SCRIPTKILL终止执行脚本。但是,为了数据完整性,此命令不保证成功终止。如果脚本在执行部分编写的逻辑时需要终止,则此命令不起作用。您需要执行SHUTDOWNno??save来终止服务器而不持久化数据以完成终止脚本。其他一些了解了以上知识后,开发一些简单的Lua脚本基本就够了。但是在实际开发中还是有一些要点。请务必对Lua脚本进行全面测试,以确保其逻辑的健壮性。当Lua脚本遇到异常时,已经执行的逻辑不会回滚。尽量不要使用Lua提供的随机函数,查看相关官方文档。不要在Lua脚本中写function函数,整个脚本作为一个函数的函数体。在脚本中声明的所有变量都使用local关键字。在集群中使用Lua脚本来保证逻辑中的所有key都分配到同一台机器,即同一个槽(slot),可以使用RedisHashTag技术。再次强调,Lua脚本不能包含过于耗时和复杂的逻辑。5.小结本文详细讲解和演示了RedisLua脚本的使用场景以及编写RedisLua脚本所需的Lua编程语法,同时也分享了在实际开发RedisLua脚本时需要注意的一些要点。希望这可以帮助您掌握这项技术。今天的分享就到这里。下次我会分享在实际Redis开发中如何使用Lua脚本,所以这篇文章一定要掌握。本文转载自微信公众号“码农小胖哥”,可通过以下二维码关注。转载本文请联系码农小胖公众号。