这篇文章主要给大家介绍了关于 Go 语言中通过 Lua 脚本操作 Redis 的方法, 文中通过示例代码介绍的非常详细, 对大家的学习或者工作具有一定的参考学习价值, 需要的朋友可以参考借鉴, 下面随着小编来一起学习学习吧
前言
为了在我的一个基本库中降低与 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 脚本支持, 默认有 KEYSARGV 两个数组, 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 的经验提供一些总结 https://redis.io/commands/eval
一段更复杂的脚本, 它要求在获取一个 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)
来源: http://www.phperz.com/article/18/0214/363146.html