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 可以相互转换。

通过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

总结

到此这篇关于Golang使用lua脚本实现redis原子操作的文章就介绍到这了,更多相关golang lua脚本实现redis原子操作内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 使用golang写一个redis-cli的方法示例

    0. redis通信协议 redis的客户端(redis-cli)和服务端(redis-server)的通信是建立在tcp连接之上, 两者之间数据传输的编码解码方式就是所谓的redis通信协议.所以,只要我们的redis-cli实现了这个协议的解析和编码,那么我们就可以完成所有的redis操作. redis 协议设计的非常易读,也易于实现,关于具体的redis通信协议请参考:通信协议(protocol).后面我们在实现这个协议的过程中也会简单重复介绍一下具体实现 1. 建立tcp连接 redis

  • golang实现redis的延时消息队列功能示例

    前言 在学习过程中发现redis的zset还可以用来实现轻量级的延时消息队列功能,虽然可靠性还有待提高,但是对于一些对数据可靠性要求不那么高的功能要求完全可以实现.本次主要采用了redis中zset中的zadd, zrangebyscore 和 zdel来实现一个小demo. 提前准备 安装redis, redis-go 因为用的是macOS, 直接 $ brew install redis $ go get github.com/garyburd/redigo/redis 又因为比较懒,生成任

  • 在Golang中使用Redis的方法示例

    周五上班的主要任务是在公司老平台上用redis处理一个队列问题,顺便复习了一下redis操作的基础知识,回来后就想着在自己的博客demo里,用redis来优化一些使用场景,学习一下golang开发下redis的使用. Redis简单介绍 简介 关于Redis的讨论,其实在现在的后台开发中已经是个老生常谈的问题,基本上也是后端开发面试的基本考察点.其中 Redis的背景介绍和细节说明在这里就不赘述.不管怎么介绍,核心在于Redis是一个基于内存的key-value的多数据结构存储,并可以提供持久化

  • Golang中如何使用lua进行扩展详解

    前言 最近在项目中需要使用lua进行扩展,发现github上有一个用golang编写的lua虚拟机,名字叫做gopher-lua.使用后发现还不错,借此分享给大家,下面话不多说了,来一起看看详细的介绍吧. 数据类型 lua中的数据类型与golang中的数据类型对应关系作者已经在文档中说明,值得注意的是类型是以L开头的,类型的名称是以LT开头的. golang中的数据转换为lua中的数据就必须转换为L开头的类型: str := "hello" num := 10 L.LString(st

  • 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 [ke

  • Go语言中通过Lua脚本操作Redis的方法

    前言 为了在我的一个基本库中降低与Redis的通讯成本,我将一系列操作封装到LUA脚本中,借助Redis提供的EVAL命令来简化操作. EVAL能够提供的特性: 可以在LUA脚本中封装若干操作,如果有多条Redis指令,封装好之后只需向Redis一次性发送所有参数即可获得结果 Redis可以保证Lua脚本运行期间不会有其他命令插入执行,提供像数据库事务一样的原子性 Redis会根据脚本的SHA值缓存脚本,已经缓存过的脚本不需要再次传输Lua代码,减少了通信成本,此外在自己代码中改变Lua脚本,执

  • 简介Lua脚本与Redis数据库的结合使用

    可能你已经听说过Redis 中嵌入了脚本语言,但是你还没有亲自去尝试吧?  这个入门教程会让你学会在你的Redis 服务器上使用强大的lua语言. Hello, Lua! 我们的第一个Redis Lua 脚本仅仅返回一个字符串,而不会去与redis 以任何有意义的方式交互. 复制代码 代码如下: local msg = "Hello, world!" return msg 这是非常简单的,第一行代码定义了一个本地变量msg存储我们的信息, 第二行代码表示 从redis 服务端返回msg

  • Redis中lua脚本实现及其应用场景

    目录 1. Redis Lua脚本概述 2. Redis Lua脚本的优势 3. Redis Lua脚本的应用场景 4. Redis Lua脚本的使用方法 5. java中使用redis的lua脚本 5.1. 添加Redis依赖 在pom.xml中添加以下依赖: 5.2. 配置Redis连接信息 在application.properties中添加以下配置: 5.3. 定义Redis Lua脚本 5.4. 实现RedisService 5.5. 编写Redis Lua脚本 5.6. 测试Redi

  • redis lua脚本实战秒杀和减库存的实现

    目录 前言 1.redisson介绍 2. redis lua脚本编写与执行 3.redis减库存lua脚本 4.实战 4.1 减库存逻辑 4.2 压测 前言 我们都知道redis是高性能高并发系统必不可少的kv中间件,它以高性能,高并发著称,我们常常用它做缓存,将热点数据或者是万年不变的数据缓存到redis中,查询的时候直接查询redis,减轻db的压力,分布式系统中我们也会拿它来做分布式锁,分布式id,幂等来解决一些分布式问题,redis也支持lua脚本,而且能够保证lua脚本执行过程中原子

  • redis执行lua脚本的实现方法

    目录 1. 语法格式 2.类型转换 3.lua脚本 3.1 script命令 3.2 脚本原子性 3.3 脚本缓存和EVALSHA 3.4 全局变量保护 3.5 日志记录 从redis 2.6.0版本开始,redis内置了Lua解释器,并提供了eval命令来解析Lua脚本求值. 1. 语法格式 语法: eval script numkeys keys args 参数: eval - redis提供解析lua脚本的命令          script - lua脚本           numke

  • 详解Redis中Lua脚本的应用和实践

    引言 前段时间组内有个投票的产品,上线前考虑欠缺,导致被刷票严重.后来,通过研究,发现可以通过 redis lua 脚本实现限流,这里将 redis lua 脚本相关的知识分享出来,讲的不到位的地方还望斧正. redis lua 脚本相关命令 这一小节的内容是基本命令,可粗略阅读后跳过,等使用的时候再回来查询 redis 自 2.6.0 加入了 lua 脚本相关的命令,EVAL.EVALSHA.SCRIPT EXISTS.SCRIPT FLUSH.SCRIPT KILL.SCRIPT LOAD,

  • Redis如何使用lua脚本实例教程

    前言 在redis的官网上洋洋洒洒的大概提供了200多个命令,貌似看起来很多,但是这些都是别人预先给你定义好的,但你却不能按照自己的意图进行定制, 所以是不是感觉自己还是有一种被束缚的感觉,有这个感觉就对了... 说来也巧,redis的大老板给了你解决这种问题的方法,那就是Lua脚本,而且redis的最新版本也支持Lua Script debug,这应该也是未来Redis的一 个发展趋势,要想学好Redis,必会Lua Script... 下面话不多说了,来一起看看详细的介绍吧 版本:自2.6.

  • Redis执行Lua脚本的好处与示例代码

    前言 Redis从2.6版本开始引入对Lua脚本的支持,通过在服务器中嵌入Lua环境,Redis客户端可以使用Lua脚本,直接在服务端原子的执行多个Redis命令. 其中,使用EVAL命令可以直接对输入的脚本进行求值: redis>EVAL "return 'hello world'" 0 "hello world" 使用脚本的好处如下: 1.减少网络开销:本来5次网络请求的操作,可以用一个请求完成,原先5次请求的逻辑放在redis服务器上完成.使用脚本,减少

  • SpringBoot+Redis执行lua脚本的方法步骤

    1.背景 有时候,我们需要一次性操作多个 Redis 命令,但是 这样的多个操作不具备原子性,而且 Redis 的事务也不够强大,不支持事务的回滚,还无法实现命令之间的逻辑关系计算.所以,一般在开发中,我们会利用 lua 脚本来实现 Redis 的事务. 2.lua 脚本 Redis 中使用 lua 脚本,我们需要注意的是,从 Redis 2.6.0后才支持 lua 脚本的执行. 使用 lua 脚本的好处: 原子操作:lua脚本是作为一个整体执行的,所以中间不会被其他命令插入. 减少网络开销:可

随机推荐