怎么在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原子操作有进一步的了解吗?如果还想了解更多知识或者相关内容,请关注恰卡编程网行业资讯频道,感谢大家的支持。

发布于 2021-03-21 22:39:31
分享
海报
157
上一篇:怎么在python中使用QDateTimeEdit日期时间控件下一篇:如何在Go语言中使用正则表达式
目录

    忘记密码?

    图形验证码