ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

go-redis 实战:用 Lua 脚本把多次 Redis 操作封装为原子命令

go-redis 实战:用 Lua 脚本把多次 Redis 操作封装为原子命令 后端数据库客户端缓存【免费下载链接】go-redisRedis Go client项目地址https://gitcode.com/GitHub_Trending/go/go-redis点击查看免费下载本文以 go-redis 官方示例 lua-scripting 为主线讲解如何在 Go 中借助redis.NewScript把 Lua 脚本封装为可复用、可安全并发调用的Redis 函数实现跨多条命令的原子操作如读-改-写计数器和多参数聚合求和。读完本文你将掌握 Lua 脚本的KEYS/ARGV约定、go-redis 的Script类型在底层如何用EVALSHAEVAL双阶段执行以及如何把脚本返回值转换为 Go 原生类型并能在自己的项目中直接落地这套模式。示例项目速览与运行方式example/lua-scripting是一个自包含的独立 Go module目录下包含 README.md、main.go 以及 go.mod / go.sum。运行前需要本机有一个监听在默认端口:6379的 Redis 实例。然后在示例目录下执行go run .程序启动后会先通过rdb.FlushDB(ctx)清空当前库示例内部约定随后依次演示两个 Lua 脚本预期输出大致为# INCR BY incr by 1: 1 incr by 5: 6 incr by 0: 6 # SUM sum is: 6从 go.mod 可以看到该示例通过replace github.com/redis/go-redis/v9 ../..指向仓库根目录即以本地源码方式引用 go-redis 主干版本在正式项目里你只需正常go get github.com/redis/go-redis/v9即可。完整代码拆解两个脚本两种用法main.go 全长仅 60 余行却覆盖了 Lua 脚本最常见的两种形态通过KEYS/ARGV传参的单键计数器以及通过可变长ARGV做循环聚合的求和器。1. 原子计数器incrByvar incrBy redis.NewScript( local key KEYS[1] local change ARGV[1] local value redis.call(GET, key) if not value then value 0 end value value change redis.call(SET, key, value) return value )这段 Lua 完成了GET → 判断 → 累加 → SET → 返回的完整业务流程。关键点KEYS[1]与ARGV[1]Redis 强制要求脚本通过KEYS数组传入键名、通过ARGV数组传入参数这是脚本可被 Redis 集群正确路由到同一哈希槽的前提下文详述。使用redis.call(GET, key)而非redis.GETLua 脚本内调用 Redis 命令的标准写法是redis.call(...)。空值保护当键不存在时GET返回falseLua 侧表现为 nil 逻辑脚本将其归一化为0避免nil change报错。全程单次执行、无中间态整个读改写过程在 Redis 服务端一次完成天然规避了多客户端并发时的竞态TOCTOU这正是把业务逻辑下沉到 Lua的核心价值。调用侧只需把键名和参数依次传入num, err : incrBy.Run(ctx, rdb, []string{my_counter}, change).Int()示例依次以1、5、0三种增量执行输出1、6、6验证了累加与返回值语义。2. 可变参数聚合sumvar sum redis.NewScript( local key KEYS[1] local sum redis.call(GET, key) if not sum then sum 0 end local num_arg #ARGV for i 1, num_arg do sum sum ARGV[i] end redis.call(SET, key, sum) return sum )这个脚本展示了 Lua 的可变参数处理#ARGV取 ARGV 数组长度配合for i 1, num_arg do ... end遍历全部入参并累加结果同样回写key并返回。调用时 Go 侧把多个参数以可变参数形式传入sum, err : sum.Run(ctx, rdb, []string{my_sum}, 1, 2, 3).Int()输出sum is: 6。注意Run的签名是Run(ctx, c, keys []string, args ...interface{})参数数量完全开放非常适合求和、批量校验、滑动窗口统计这类入参不定长的场景。底层原理Script 如何把脚本变成命令redis.NewScript返回的是仓库根目录 script.go 中定义的*redis.Script。理解它的执行策略是高效使用 Lua 脚本的关键。构造期客户端计算 SHA-1func NewScript(src string) *Script { h : sha1.New() _, _ io.WriteString(h, src) return Script{ src: src, hash: hex.EncodeToString(h.Sum(nil)), ... } }NewScript在客户端用 SHA-1 对脚本源码求摘要得到hash字段。这意味着你不需要手工调用SCRIPT LOADgo-redis 已经把摘要与源码打包在Script对象里。执行期EVALSHA 优先NOSCRIPT 回退 EVALRun是日常最常用的入口script.go 的实现采用乐观 EVALSHA策略func (s *Script) Run(ctx context.Context, c Scripter, keys []string, args ...interface{}) *Cmd { r : s.EvalSha(ctx, c, keys, args...) if isNoScriptErr(r.Err()) { return s.Eval(ctx, c, keys, args...) } return r }其工作流程为先用EVALSHA sha1 numkeys KEYS... ARGS...按摘要调用脚本——Redis 会缓存已执行过的脚本此路径带宽开销最小若返回NOSCRIPT例如服务器重启、执行了SCRIPT FLUSH或脚本从未被加载过isNoScriptErr捕获该错误立即回退到EVAL发送完整源码让脚本在服务端缓存并正常执行。isNoScriptErrscript.go同时识别已归一化的ErrNoScript与原始NOSCRIPT前缀错误覆盖了正常同步执行与自动流水线autopipeline等异步场景。NOSCRIPT 错误归一化在 scripting_commands.go 的eval底层实现中go-redis 会把服务端返回的NOSCRIPT归一化为ErrNoScript供Script.Run判断而EvalSha/EvalShaRO命令本身则会把NOSCRIPT作为错误原样暴露给调用者——这在 commands_test.go 的测试中得到了验证对一个随机摘要执行EvalSha会得到redis.ErrNoScript。返回值转换Run返回*Cmd示例中通过.Int()直接取整数。*Cmd还提供.Result()、.Text()、.Float64()、.Slice()等丰富的取值方法覆盖脚本返回整数、字符串、数组乃至嵌套数组如return {KEYS[1],ARGV[1]}的场景。仓库 commands_test.go 中client.Eval(ctx, return {KEYS[1],ARGV[1]}, []string{key}, hello)返回[]interface{}{key, hello}展示了多值返回的解析方式。面向脚本的命令族Eval / EvalSha / SCRIPT 管理除了Script.Run高层封装go-redis 在 scripting_commands.go 中暴露了完整的一等命令 APIAPI对应 Redis 命令说明Eval(ctx, script, keys, args...)EVAL直接执行脚本源码EvalSha(ctx, sha1, keys, args...)EVALSHA按摘要执行已缓存脚本EvalRO(ctx, script, keys, args...)EVAL_RO只读执行Redis 7.0禁止写命令EvalShaRO(ctx, sha1, keys, args...)EVALSHA_RO只读按摘要执行ScriptLoad(ctx, script)SCRIPT LOAD预加载脚本并返回摘要ScriptExists(ctx, hashes...)SCRIPT EXISTS批量检查脚本是否已缓存ScriptFlush(ctx)SCRIPT FLUSH清空脚本缓存ScriptKill(ctx)SCRIPT KILL终止长时间运行的脚本从 scripting_commands.go 的eval实现可以看到go-redis 在构造命令参数时会按EVAL script numkeys KEYS... ARGS...的协议顺序组包并设置cmd.SetFirstKeyPos(3)——第一个键从第 3 个参数索引 3开始这为集群路由将涉及同一哈希槽的 key 路由到同一节点提供了依据。因此即使不依赖Script封装你也可以直接res, err : rdb.Eval(ctx, return redis.call(INCR, KEYS[1]), []string{k}, nil).Int()集群与多键脚本的正确姿势Lua 脚本在集群模式下要求脚本中出现的所有键必须位于同一哈希槽即同一节点。go-redis 的Eval/FCall通过SetFirstKeyPos(3)声明键的起始位置让集群路由层能正确识别脚本键并进行哈希槽计算相关实现见 osscluster_router.go 与 internal/routing。因此键名尽量使用统一的哈希标签如{user:1}:counter确保跨键脚本可路由示例中的my_counter、my_sum均为单键脚本天然满足约束。进阶只读执行与服务端摘要在 script.go 中还提供了两个进阶能力RunRO/EvalRO/EvalShaRO对应 Redis 7.0 的EVAL_RO/EVALSHA_RO只读脚本可安全地路由到副本节点适合缓存预热、只读聚合等场景NewScriptServerSHA当客户端不便或不希望在本地计算 SHA-1 时可改用服务端摘要——首次调用通过SCRIPT LOAD从 Redis 获取摘要并缓存NOSCRIPT时自动重新加载并重试一次。该路径的行为在 script_server_sha_unit_test.go 与 script_server_sha_integration_test.go 中有完整的单测与集成测试覆盖。更多实践官方测试中的脚本范例仓库 example_test.go 中的ExampleScript是另一个简洁范本——键存在才自增的条件脚本IncrByXX : redis.NewScript( if redis.call(GET, KEYS[1]) ~ false then return redis.call(INCRBY, KEYS[1], ARGV[1]) end return false ) n, err : IncrByXX.Run(ctx, rdb, []string{xx_counter}, 2).Result()它演示了脚本返回false时 go-redis 会将其解析为redis: nil错误的语义输出nil redis: nil以及键存在时直接复用服务端INCRBY的写法。这提示我们Lua 脚本不必从零实现一切完全可以业务编排 复用原生命令。小结通过 lua-scripting 示例可以看到go-redis 的 Lua 脚本支持是一个高性价比的能力redis.NewScriptRun一行式调用即可获得原子性EVALSHA优先、NOSCRIPT回退EVAL的机制让带宽与正确性兼得而Eval/EvalSha/Script*命令族则为精细控制脚本生命周期提供了完整 API。当你的业务需要多条命令原子完成计数器、限流、分布式锁、批量校验时把逻辑下沉到 Lua 脚本是比事务MULTI/EXEC更灵活、比 WATCH 重试更简洁的工程选择。赞分享后端数据库客户端缓存【免费下载链接】go-redisRedis Go client项目地址https://gitcode.com/GitHub_Trending/go/go-redis点击查看免费下载相关推荐NeteaseMusic终极第三方Web音乐播放器一站式融合网易云与QQ音乐资源NeteaseMusic终极第三方Web音乐播放器一站式融合网易云与QQ音乐资源 NeteaseMusic是一款功能强大的第三方Web音乐播放器它巧妙地整Scrapy-Redis高级主题利用Lua脚本优化Redis操作性能Scrapy Redis高级主题利用Lua脚本优化Redis操作性能 在大规模分布式爬虫系统中Scrapy Redis作为基于Redis的Scrapy组件扩后端消息队列数据工程5分钟上手go-redis扩展自定义命令与Lua脚本实战指南5分钟上手go redis扩展自定义命令与Lua脚本实战指南 你是否遇到过Redis官方命令无法满足业务需求的困境是否需要在Go项目中高效扩展Redis功能后端数据库客户端缓存上一篇Temporal Python SDK活动超时处理超时类型对比下一篇GPTFast中的随机数管理采样函数设计与结果可复现性创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表