怎么在Golang中利用lua脚本实现redis原子操作
今天就跟大家聊聊有关怎么在Golang中利用lua脚本实现redis原子操作,可能很多人都不太了解,为了让大家更加了解,小编给大家总结了以下内容,希望大家根据这篇文章可以有所收获。
目录
[redis 调用Lua脚本](#redis 调用Lua脚本)
[redis+lua 实现评分排行榜实时更新](#redis+lua 实现评分排行榜实时更新)
[lua 脚本](#lua 脚本) Golang调用redis+lua示例 byte切片与string的转换优化
redis 调用Lua脚本
EVAL命令
redis调用Lua脚本需要使用EVAL命令。
redis EVAL命令格式:
redis 127.0.0.1:6379> EVAL script numkeys key [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系统下,则需要在git bash中执行,在powershell中无法读取value):
***@LAPTOP-V7V47H0LMINGW64/d/study/code/lua $redis-cli.exe-a123--evaltest.luatestkey,hello hello
test.lua如下(redis log打印在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命令,用于对于脚本子系统进行控制:
script flush:清除所有的脚本缓存
script load:将脚本装入脚本缓存,不立即运行并返回其校验和
script exists:根据指定脚本校验和,检查脚本是否存在于缓存
script kill:杀死当前正在运行的脚本(防止脚本运行缓存,占用内存)
主要优势: 减少网络开销:多个请求通过脚本一次发送,减少网络延迟
原子操作:将脚本作为一个整体执行,中间不会插入其他命令,无需使用事务
复用:客户端发送的脚本永久存在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命令: ZCARD key 获取有序集合的成员数
ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT] 通过分数返回有序集合指定区间内的成员(从小到大的顺序)
ZREMRANGEBYRANK key start stop 移除有序集合中给定的排名区间的所有成员
ZADD key score1 member1 [score2 member2] 向有序集合添加一个或多个成员,或者更新已存在成员的分数
主要思路是维护一个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 }
看完上述内容,你们对怎么在Golang中利用lua脚本实现redis原子操作有进一步的了解吗?如果还想了解更多知识或者相关内容,请关注恰卡编程网行业资讯频道,感谢大家的支持。
推荐阅读
Golang中的nil和零值怎么声明
Golang中的nil和零值怎么声明这篇文章主要讲解了“Golan...
golang Gob怎么使用
golangGob怎么使用本篇内容介绍了“golangGob怎...
golang切片长度与容量指的是什么
golang切片长度与容量指的是什么今天小编给大家分享一下gola...
golang循环遍历map的方式有哪些
golang循环遍历map的方式有哪些本篇内容介绍了“golang...
golang如何添加list元素
golang如何添加list元素这篇“golang如何添加list...
golang包的特性有哪些
golang包的特性有哪些这篇文章主要介绍了golang包的特性有...
如何从 PHP 过渡到 Golang?
我是PHP开发者,转Go两个月了吧,记录一下使用Golang怎么一步步开发新项目。本着有坑填坑,有错改错的宗...
golang web从入门到精通 获取请求头信息、GET(POST)请求参数
适合PHP转Golang的函数类库
近几年Golang越来越火了,很多之前写PHP的也开始投入到Golang的怀抱中,今天小编给大家推荐一个挺好用的适合php转go的...
Golang编程 golang实现php函数json_decode()