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

GrassInWind2019 · · 6028 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

# 目录 - [redis 调用Lua脚本](#redis 调用Lua脚本) [EVAL命令](#EVAL命令) [SCRIPT命令](#SCRIPT命令) - [redis+lua 实现评分排行榜实时更新](#redis+lua 实现评分排行榜实时更新) [lua 脚本](#lua 脚本) [ Golang调用redis+lua示例](#Golang调用redis+lua示例) [ byte切片与string的转换优化](#byte切片与string的转换优化) [ github链接](#github链接) - [参考文献](#参考文献) # redis 调用Lua脚本 redis确保一条Lua script脚本执行期间,其它任何脚本或者命令都无法执行,这样保证了脚本执行的原子性。 ## 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.Pointer.png](https://static.studygolang.com/200228/814c8631395ed8c7e70eaa355df5a0ad.png) 通过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 <iframe src="https://ghbtns.com/github-btn.html?user=GrassInWind2019&repo=bookms&type=watch&count=true&size=large" allowtransparency="true" frameborder="0" scrolling="0" width="156px" height="30px"></iframe> <iframe src="https://ghbtns.com/github-btn.html?user=GrassInWind2019&repo=bookms&type=fork&count=true&size=large" allowtransparency="true" frameborder="0" scrolling="0" width="156px" height="30px"></iframe> # 参考文献: 深度解密Go语言之unsafe https://www.sohu.com/a/319106990_657921

有疑问加站长微信联系(非本文作者))

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

6028 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传