Golang使用lua脚本实现redis原子操作
目录
- [redis调用Lua脚本](#redis调用Lua脚本)
- [redis+lua实现评分排行榜实时更新](#redis+lua实现评分排行榜实时更新)
[lua脚本](#lua脚本)
Golang调用redis+lua示例
byte切片与string的转换优化
redis调用Lua脚本
EVAL命令
redis调用Lua脚本需要使用EVAL命令。
redisEVAL命令格式:
redis127.0.0.1:6379>EVALscriptnumkeyskey[key...]arg[arg...]
最简单的例子:
127.0.0.1:6379>eval"return{'Hello,GrassInWind!'}"0 1)"Hello,GrassInWind!" 127.0.0.1:6379>eval"returnredis.call('set',KEYS[1],'bar')"1foo OK
使用redis-cli调用lua脚本示例(若在windows系统下,则需要在gitbash中执行,在powershell中无法读取value):
***@LAPTOP-V7V47H0LMINGW64/d/study/code/lua $redis-cli.exe-a123--evaltest.luatestkey,hello hello
test.lua如下(redislog打印在server的日志中):
localkey,value=KEYS[1],ARGV[1] redis.log(redis.LOG_NOTICE,"key=",key,"value=",value) redis.call('SET',key,value) locala=redis.call('GET',key) returna
SCRIPT命令
redis提供了以下几个script命令,用于对于脚本子系统进行控制:
scriptflush:清除所有的脚本缓存
scriptload:将脚本装入脚本缓存,不立即运行并返回其校验和
scriptexists:根据指定脚本校验和,检查脚本是否存在于缓存
scriptkill:杀死当前正在运行的脚本(防止脚本运行缓存,占用内存)
主要优势:减少网络开销:多个请求通过脚本一次发送,减少网络延迟
原子操作:将脚本作为一个整体执行,中间不会插入其他命令,无需使用事务
复用:客户端发送的脚本永久存在redis中,其他客户端可以复用脚本
可嵌入性:可嵌入JAVA,C#等多种编程语言,支持不同操作系统跨平台交互
通过script命令加载及执行lua脚本示例:
127.0.0.1:6379>scriptload"return'HelloGrassInWind'" "c66be1d9b54b3182f8d8e12f8b01a4e5c7c4af5b" 127.0.0.1:6379>scriptexists"c66be1d9b54b3182f8d8e12f8b01a4e5c7c4af5b" 1)(integer)1 127.0.0.1:6379>evalsha"c66be1d9b54b3182f8d8e12f8b01a4e5c7c4af5b"0 "HelloGrassInWind" 127.0.0.1:6379>scriptflush OK 127.0.0.1:6379>scriptexists"c66be1d9b54b3182f8d8e12f8b01a4e5c7c4af5b" 1)(integer)0
#redis+lua实现评分排行榜实时更新
使用redis的zset保存排行数据,使用lua脚本实现评分排行更新的原子操作。
lua脚本
相关redis命令:ZCARDkey获取有序集合的成员数
ZRANGEBYSCOREkeyminmax[WITHSCORES][LIMIT]通过分数返回有序集合指定区间内的成员(从小到大的顺序)
ZREMRANGEBYRANKkeystartstop移除有序集合中给定的排名区间的所有成员
ZADDkeyscore1member1[score2member2]向有序集合添加一个或多个成员,或者更新已存在成员的分数
主要思路是维护一个zset,将评分前N位保存到redis中,当成员的评分发生变化时,动态更新zset的成员信息。
lua脚本如下,其中KEYS[1]表示zset的key,ARGV[1]表示期望的zset最大存储成员数量,ARGV[2]表示评分上限,默认评分下限是0,ARGV[3]表示待添加的评分,ARGV[4]表示待添加的成员名称。
--rediszsetoperations --argv[capacitymaxScorenewMemberScoremember] --执行示例redis-cli.exe--evalzsetop.luamtest,355test1 --获取键和参数 localkey,cap,maxSetScore,newMemberScore,member=KEYS[1],ARGV[1],ARGV[2],ARGV[3],ARGV[4] redis.log(redis.LOG_NOTICE,"key=",key,",cap=",cap,",maxSetScore=",maxSetScore,",newMemberScore=",newMemberScore,",member=",member) locallen=redis.call('zcard',key); --lenneednotnil,otherwisewilloccur"attempttocomparenilwithnumber" iflenthen iftonumber(len)>=tonumber(cap) then localnum=tonumber(len)-tonumber(cap)+1 locallist=redis.call('zrangebyscore',key,0,maxSetScore,'limit',0,num) redis.log(redis.LOG_NOTICE,"key=",key,"maxSetScore=",maxSetScore,"num=",num) fork,lowestScoreMemberinpairs(list)do locallowestScore=redis.call('zscore',key,lowestScoreMember) redis.log(redis.LOG_NOTICE,"list:",lowestScore,lowestScoreMember) iftonumber(newMemberScore)>tonumber(lowestScore) then localrank=redis.call('zrevrank',key,member) --rankisnilindicatenewmemberisnotexistinset,needremovethelowestscoremember ifnotrankthen localindex=tonumber(len)-tonumber(cap); redis.call('zremrangebyrank',key,0,index) end redis.call('zadd',key,newMemberScore,member); break end end else redis.call('zadd',key,newMemberScore,member); end end
Golang调用redis+lua示例
init函数中读取Lua脚本并通过redisgo包的NewScript函数加载这个脚本,在使用时通过返回的指针调用lua.Do()即可。
funcinit(){ ... file,err:=os.Open(zsetopFileName) iferr!=nil{ panic("open"+zsetopFileName+""+err.Error()) } bytes,err:=ioutil.ReadAll(file) iferr!=nil{ panic(err.Error()) } zsetopScript=utils.UnsafeBytesToString(bytes) logs.Debug(zsetopScript) lua=redis.NewScript(1,zsetopScript) } funcZaddWithCap(key,memberstring,scorefloat32,maxScore,capint)(replyinterface{},errerror){ c:=pool.Get() //DooptimisticallyevaluatesthescriptusingtheEVALSHAcommand.Ifscriptnotexist,willuseevalcommand. reply,err=lua.Do(c,key,cap,maxScore,score,member) return }
redisgo包对Do方法做了优化,会检查这个脚本的SHA是否存在,若不存在,会通过EVAL命令执行即会加载脚本,下次执行就可以通过
EVALSHA来执行了。
func(s*Script)Do(cConn,keysAndArgs...interface{})(interface{},error){ v,err:=c.Do("EVALSHA",s.args(s.hash,keysAndArgs)...) ife,ok:=err.(Error);ok&&strings.HasPrefix(string(e),"NOSCRIPT"){ v,err=c.Do("EVAL",s.args(s.src,keysAndArgs)...) } returnv,err }
byte切片与string的转换优化
在Go读取了脚本内容存在byte切片中,需要转化为string来调用redis.NewScript来创建对象。
通过unsafe包转化可以避免内存拷贝从而提高效率。
unsafe包提供了2点重要的能力:任何类型的指针和unsafe.Pointer可以相互转换。uintptr类型和unsafe.Pointer可以相互转换。
通过unsafe包将byte切片转换为string示例:
funcUnsafeBytesToString(bytes[]byte)string{ hdr:=&reflect.StringHeader{ Data:uintptr(unsafe.Pointer(&bytes[0])), Len:len(bytes), } return*(*string)(unsafe.Pointer(hdr)) }
string与slice底层结构如下:
typeSliceHeaderstruct{ Datauintptr Lenint Capint } typeStringHeaderstruct{ Datauintptr Lenint }
github链接
详见https://github.com/GrassInWind2019/bookms
总结
到此这篇关于Golang使用lua脚本实现redis原子操作的文章就介绍到这了,更多相关golanglua脚本实现redis原子操作内容请搜索毛票票以前的文章或继续浏览下面的相关文章希望大家以后多多支持毛票票!
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。