欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

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

程序员文章站 2023-01-13 10:31:25
前言 为了在我的一个基本库中降低与redis的通讯成本,我将一系列操作封装到lua脚本中,借助redis提供的eval命令来简化操作。 eval能够提供的特性:...

前言

为了在我的一个基本库中降低与redis的通讯成本,我将一系列操作封装到lua脚本中,借助redis提供的eval命令来简化操作。

eval能够提供的特性:

  • 可以在lua脚本中封装若干操作,如果有多条redis指令,封装好之后只需向redis一次性发送所有参数即可获得结果
  • redis可以保证lua脚本运行期间不会有其他命令插入执行,提供像数据库事务一样的原子性
  • redis会根据脚本的sha值缓存脚本,已经缓存过的脚本不需要再次传输lua代码,减少了通信成本,此外在自己代码中改变lua脚本,执行时redis必定也会使用最新的代码。

导入常见的go库如 "github.com/go-redis/redis",就可以实现以下代码。

生成一段lua脚本

// keys: key for record
// argv: fieldname, currentunixtimestamp, recordttl
// update expire field of record key to current timestamp, and renew key expiration
var updaterecordexpirescript = redis.newscript(`
redis.call("expire", keys[1], argv[3])
redis.call("hset", keys[1], argv[1], argv[2])
return 1
`)

该变量创建时,lua代码不会被执行,也不需要有已存的redis连接。

redis提供的lua脚本支持,默认有keys、argv两个数组,keys代表脚本运行时传入的若干键值,argv代表传入的若干参数。由于lua代码需要保持简洁,难免难以读懂,最好为这些参数写一些注释

注意:上面一段代码使用``跨行,`所在的行虽然空白回车,也会被认为是一行,报错时不要看错代码行号。

运行一段lua脚本

 updaterecordexpirescript.run(c.client, []string{recordkey(key)}, 
         expirefield,
         time.now().utc().unixnano(), int64(c.opt.recordttl/time.second)).err()

运行时,run将会先通过evalsha尝试通过缓存运行脚本。如果没有缓存,则使用eval运行,这时lua脚本才会被整个传入redis。

lua脚本的限制

  • redis不提供引入额外的包,例如os等,只有redis这一个包可用。
  • lua脚本将会在一个函数中运行,所有变量必须使用local声明
  • return返回多个值时,redis将会只给你第一个

脚本中的类型限制

  • 脚本返回nil时,go中得到的是err = redis.nil(与get找不到值相同)
  • 脚本返回false时,go中得到的是nil,脚本返回true时,go中得到的是int64类型的1
  • 脚本返回{"ok": ...}时,go中得到的是redis的status类型(true/false)
  • 脚本返回{"err": ...}时,go中得到的是err值,也可以通过return redis.error_reply("my error")达成
  • 脚本返回number类型时,go中得到的是int64类型
  • 传入脚本的keys/argv中的值一律为string类型,要转换为数字类型应当使用to_number

如果脚本运行了很久会发生什么?

lua脚本运行期间,为了避免被其他操作污染数据,这期间将不能执行其它命令,一直等到执行完毕才可以继续执行其它请求。当lua脚本执行时间超过了lua-time-limit时,其他请求将会收到busy错误,除非这些请求是script kill(杀掉脚本)或者shutdown nosave(不保存结果直接关闭redis)

更多内容参考以下地址,我这里主要是根据使用go的经验提供一些总结。

一段更“复杂”的脚本,它要求在获取一个key值时,如果该值访问较多,就延长生存周期。此外还要比较更新时间,如果不需要更新,则直接返回取到的值,否则返回redis.nil

// keys: rec:key, key
// argv: currentunixtimestamp, hothit, recordttl, ttl
// when there's a hit,
var fetchrecordscript = redis.newscript(local value = redis.call("get", keys[2]) if(value == nil) then return nil end local hit = redis.call("hincrby", keys[1], "hit", 1) redis.call("expire", keys[1], argv[3]) local minhothit = tonumber(argv[2]) local keyttl = tonumber(argv[4]) if(hit > minhothit)then keyttl = keyttl * 2 end redis.call("expire", keys[2], keyttl) local expire = tonumber(redis.call("hget", keys[1], "expire")) local unixtime = tonumber(argv[1]) if(expire == nil or expire < unixtime) then return nil else return value end)
// keys: key for record
// argv: fieldname, currentunixtimestamp, recordttl
// update expire field of record key to current timestamp, and renew key expiration
var updaterecordexpirescript = redis.newscript(redis.call("expire", keys[1], argv[3]) redis.call("hset", keys[1], argv[1], argv[2]) return 1)

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对的支持。