网络编程
位置:首页>> 网络编程>> Go语言>> Golang使用lua脚本实现redis原子操作

Golang使用lua脚本实现redis原子操作

作者:GrassInWind2019  发布时间:2023-09-03 05:55:20 

标签: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 "return redis.call('set',KEYS[1],'bar')" 1 foo
OK

使用redis-cli调用lua脚本示例(若在windows系统下,则需要在git bash中执行,在powershell中无法读取value):


***@LAPTOP-V7V47H0L MINGW64 /d/study/code/lua
$ redis-cli.exe -a 123 --eval test.lua testkey , hello
hello

test.lua如下(redis log打印在server的日志中):


local key,value = KEYS[1],ARGV[1]
redis.log(redis.LOG_NOTICE, "key=", key, "value=", value)
redis.call('SET', key, value)
local a = redis.call('GET', key)
return a

SCRIPT命令

redis提供了以下几个script命令,用于对于脚本子系统进行控制:

script flush:清除所有的脚本缓存

script load:将脚本装入脚本缓存,不立即运行并返回其校验和

script exists:根据指定脚本校验和,检查脚本是否存在于缓存

script kill:杀死当前正在运行的脚本(防止脚本运行缓存,占用内存)

主要优势: 减少网络开销:多个请求通过脚本一次发送,减少网络延迟

原子操作:将脚本作为一个整体执行,中间不会插入其他命令,无需使用事务

复用:客户端发送的脚本永久存在redis中,其他客户端可以复用脚本

可嵌入性:可嵌入JAVA,C#等多种编程语言,支持不同操作系统跨平台交互

通过script命令加载及执行lua脚本示例:


127.0.0.1:6379> script load "return 'Hello GrassInWind'"
"c66be1d9b54b3182f8d8e12f8b01a4e5c7c4af5b"
127.0.0.1:6379> script exists "c66be1d9b54b3182f8d8e12f8b01a4e5c7c4af5b"
1) (integer) 1
127.0.0.1:6379> evalsha "c66be1d9b54b3182f8d8e12f8b01a4e5c7c4af5b" 0
"Hello GrassInWind"
127.0.0.1:6379> script flush
OK
127.0.0.1:6379> script exists "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]表示待添加的成员名称。


-- redis zset operations
-- argv[capacity maxScore newMemberScore member]
-- 执行示例 redis-cli.exe --eval zsetop.lua mtest , 3 5 5 test1
-- 获取键和参数
local key,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)
local len = redis.call('zcard', key);
-- len need not nil, otherwise will occur "attempt to compare nil with number"
if len then
if tonumber(len) >= tonumber(cap)
then
 local num = tonumber(len)-tonumber(cap)+1
 local list = redis.call('zrangebyscore',key,0,maxSetScore,'limit',0,num)
 redis.log(redis.LOG_NOTICE,"key=",key,"maxSetScore=",maxSetScore, "num=",num)
 for k,lowestScoreMember in pairs(list) do
  local lowestScore = redis.call('zscore', key,lowestScoreMember)
  redis.log(redis.LOG_NOTICE, "list: ", lowestScore, lowestScoreMember)
  if tonumber(newMemberScore) > tonumber(lowestScore)
  then
   local rank = redis.call('zrevrank',key,member)
   -- rank is nil indicate new member is not exist in set, need remove the lowest score member
   if not rank then
    local index = 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()即可。


func init() {
...
file, err := os.Open(zsetopFileName)
if err != nil {
panic("open"+zsetopFileName+" "+err.Error())
}
bytes,err := ioutil.ReadAll(file)
if err != nil {
panic(err.Error())
}
zsetopScript = utils.UnsafeBytesToString(bytes)
logs.Debug(zsetopScript)
lua =redis.NewScript(1,zsetopScript)
}
func ZaddWithCap(key,member string, score float32, maxScore, cap int) (reply interface{}, err error) {
c := pool.Get()
//Do optimistically evaluates the script using the EVALSHA command. If script not exist, will use eval command.
reply, err= lua.Do(c,key,cap,maxScore,score,member)
return
}

redisgo包对Do方法做了优化,会检查这个脚本的SHA是否存在,若不存在,会通过EVAL命令执行即会加载脚本,下次执行就可以通过

EVALSHA来执行了。


func (s *Script) Do(c Conn, keysAndArgs ...interface{}) (interface{}, error) {
v, err := c.Do("EVALSHA", s.args(s.hash, keysAndArgs)...)
if e, ok := err.(Error); ok && strings.HasPrefix(string(e), "NOSCRIPT ") {
v, err = c.Do("EVAL", s.args(s.src, keysAndArgs)...)
}
return v, err
}

byte切片与string的转换优化

在Go读取了脚本内容存在byte切片中,需要转化为string来调用redis.NewScript来创建对象。

通过unsafe包转化可以避免内存拷贝从而提高效率。

unsafe 包提供了 2 点重要的能力: 任何类型的指针和 unsafe.Pointer 可以相互转换。 uintptr 类型和 unsafe.Pointer 可以相互转换。

Golang使用lua脚本实现redis原子操作

通过unsafe包将byte切片转换为string示例:


func UnsafeBytesToString(bytes []byte) string {
hdr := &reflect.StringHeader{
Data: uintptr(unsafe.Pointer(&bytes[0])),
Len: len(bytes),
}
return *(*string)(unsafe.Pointer(hdr))
}

string与slice底层结构如下:


type SliceHeader struct {
Data uintptr
Len int
Cap int
}
type StringHeader struct {
Data uintptr
Len int
}

github链接

详见https://github.com/GrassInWind2019/bookms

来源:https://studygolang.com/articles/26844

0
投稿

猜你喜欢

  • 如何用Access加密页面?很简单哦,看看这个用用Access和ASP做的加密程序: <%userid =&nbs
  • 前几天在“CSS那些事儿”的群中,一位读者朋友(小土豆)问我书中提到首字下沉的时候为什么要增加一个清除浮动。当时我自己一时迷惑了,为什么呢,
  • HTML5,被传为Flash 的杀手,是一种用于web 应用程序开发、具有变革意义的网络技术。HTML 5提供了一些新的元素和属性,其中有些
  • 插入排序 插入排序是这样实现的:  首先新建一个空列表,用于保存已排序的有序数列(我们称之为"有序列表")。
  • 有两个服务器,装了两个数据库,一个是主的,一个是备用的,下面的的功能就将主数据库的数据库,实时同步到备份数据库上,使他们的数据内容,基本上保
  • 前言这次,我们要用Pygame写一个Pong游戏先看看效果:需要的模块:Pygame在python文件同目录下新建resources文件夹,
  • “In the latest release 10.2 Oracle changed these default values. The m
  • 昨天看到设计师提供的一张有关多个设计师角色间的漫画图(如下图),着实感到有点讽刺。现在的设计还只是停留在“盲人摸象”的阶段,为什么会这样?在
  • ChatGPT 是 OpenAI 开发的 GPT(Generative Pre-trained Transformer)语言模型的变体。它是
  • 【原文地址】My "First Look at Orcas" Presentation 【原文发表日期】 Th
  • 数据库优化是一项很复杂的工作,因为这最终需要对系统优化的很好理解才行。尽管对系统或应用系统的了解不多的情况下优化效果还不错,但是如果想优化的
  • 一、is_numberic函数简介国内一部分CMS程序里面有用到过is_numberic函数,我们先看看这个函数的结构bool is_num
  • 新云4.0模版标签是全新改的了,加了前缀。如果你怀旧,请查看新云CMS3.1常用模板标签。下面的标签说明,后台就有,为了方便查看转到这里。{
  • (注:在看到大家如此关注JS里头的这几个对象,我试着把原文再修改一下,力求能再详细的阐明个中意义 2007-05-21)在提到上述的概念之前
  • 不同于其他软件项目,互联网项目的开发有其独有的特性。互联网项目开发不同于传统软件项目开发不同于需求定制性的软件开发公司。客户的需求是明确的,
  • 先来看一张简单的文档树很明显树的顶层节点是NodeA节点,接下来可以通过指定的合适节点移动到树中的任何点,结合以下的代码你可以更好的了解这棵
  • 西贝做了许久的交互设计工作,每年的目标都有不同,却发现今年没有什么提高和改进的地方。也许是自己没有回头总结,总是被这样那样的借口推脱。最近休
  • tbody 标签表格主体(正文)。该标签用于组合 HTML 表格的主体内容。tbody 元素应该与&
  • 讲这个方法之前,我们应该先了解下插入节点时浏览器会做什么。在浏览器中,我们一旦把节点添加到document.body(或者其他节点)中,页面
  • 过去一段时间人们似乎又非常热衷于探讨网络文档的印刷格式,涌现了很多与之相关的技术与理论资料,其中相当重要的一个领域就是关于印刷中字号和行高的
手机版 网络编程 asp之家 www.aspxhome.com