redis 事务有两种实现方式,一种是通过 MULTI
命令 , 另一种是使用 lua script. lua script 更简单, 而且 lua script 由于交互更少,且 redis 可以缓存 lua script,因此,效率更高,非常适合用于追求效率的地方。
lua script
官方文档: https://redis.io/commands/eval
redis的lua脚本执行有两个命令,一个是eval
, 另一个是 evalsha
.
eval 命令的语法:
> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2], ARGV[3]}" 2 key1 key2 first second third
1) "key1"
2) "key2"
3) "first"
4) "second"
5) "third"
说明:
- 第二个字符串参数是执行的 lua 脚本,其中 KEYS ARGV 是关键字;
- 2 是指在 lua 脚本中,引用了两个 KEYS
- KEYS 用于指定 redis 的 key
- ARGV 就是后面的first second third 参数,在 KEYS 之后,序号从1开始
- return 是返回的数据
如果服务端已经缓存了 lua 脚本,可以使用 evalsha
来执行,当脚本内容很多时,这样,传输的数据更少。
从redis 3.2.0开始,redis 中集成了 lua 调试工具,参考这里: https://redis.io/topics/ldb
使用示例
import "github.com/gomodule/redigo/redis"
// redis lua 脚本, 确保预留分配多个时的原子操作
func ReserveUserToken(conn redis.Conn, accountID uint64, howmany uint32) (latest uint64, err error) {
ss := `local sid=redis.call('HGET',KEYS[1],'latest') or 0; `
for i := 0; i < int(howmany); i++ {
ss += `redis.call('HSET', KEYS[1], sid, ARGV[1]); sid=sid+1; `
}
// 更新 latest 值
ss += `redis.call('HSET', KEYS[1], 'latest', sid); return sid`
script := redis.NewScript(1, ss)
key := fmt.Sprintf("userTokens:%d", accountID)
ts := time.Now().Unix()
resp, err := script.Do(conn, key, ts)
if err != nil {
return
}
latest = uint64(resp.(int64))
return
}
上述代码作用如下:
- 在 redis 上为每个用户生成 hashmap
userToken:{accountId}
, 如果没有,创建该hashmap,且将 hashmap 的 key latest 设置为 0; - 如果 howmany 不为0,则为用户预留 token, 预留的方式为:
- 从 latest 增加1 作为key, 写入 hashmap 中
- latest 自增
- 循环操作
- 将最新的 latest 写入 hashmap,并返回
值得注意的是,local sid=redis.call('HGET',KEYS[1],'latest') or 0;
这里一定要有最后的 or 0
, 否则 sid 会是一个bool
类型,无法进行数值运算,会提示如下错误:
(error) ERR Error running script (call to f_2bd1a2e80fc04313100937b7b533da6bea026773): @user_script:1: user_script:1: attempt to perform arithmetic on local 'sid' (a boolean value)
几点补充
lua 判断
lua脚本判断的语法为:
if cond then
clause
else
clause
end
如果没有else,可以简化为:
if cond then
clause
end
当然,还有 elif 的语法。
整数与字符串
由于 redis 中的 key 是字符串类型,而 value 可以是整数。当你的key也是整数时,使用key与value相比时,将无法得到预期的结果,这是因为key为字符串类型,如果需要比较,可以用两种方法:
- 使用
tonumber
来转换 - 使用 +0 来强制转换
json 处理
redis中的lua脚本内置了cjson库,可以在lua脚本中直接使用cjson encode和decode来操作json数据。
encode 例子如下:
local user={name=ARGV[1],year=ARGV[2],address=ARGV[3]};
local userJson=cjson.encode(user);
if redis.call('HSET', 'users', ARGV[1], userJson) == 0 then
return -1
else
return userJson
end
decode 类似:
local userJson = redis.call('HSET', 'users', ARGV[1]);
local user = cjson.decode(userJson)