实验环境:
redis: 6.0.9
我们知道, 使用lua脚本可以在执行一串redis命令时, 实现一定原子性(lua脚本中多条指令执行过程中不会被插入新的指令), 但是并不能在命令执行出错时回滚之前的结果, 如下示例:
demo.lua
redis.call('get', 'xx')
redis.call('set', 'a1', 'b1')
redis.call('set', 'a2')
显然最后的set a2
是有语法错误的, 在执行前先确认下数据库是空的:
127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379>
然后执行下demo.lua(测试用的redis-server密码是123456):
$ redis-cli -a '123456' --eval demo.lua
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
(error) ERR Error running script (call to f_c49b51d8e861ccfd226dedb5ed80d1bd3c257235): @user_script:7: @user_script: 7: Wrong number of args calling Redis command From Lua script
上述demo.lua最后一条命令执行报错了, 我们看下数据库, 发现set a1 b1
还是写进去了, 并没有回退
127.0.0.1:6379> keys *
1) "a1"
127.0.0.1:6379> get a1
"b1"
127.0.0.1:6379>
小结:
redis在执行lua脚本在执行出错(往往是语法错误: 如参数个数不对, 类型不对)时, 已经执行的结果不会回滚.
为了避免一些低级的错误, 我们在使用编程语言(如go, java)中利用lua脚本实现一些较复杂的功能时, 需要对lua脚本先进行调试. redis-cli工具自带了调试功能. 用一个例子来说明:
简单介绍下需求:
红包活动, 业务上需要控制红包个数(每个红包多少钱由业务上控制, 脚本不用管).
保证不能发超, 且每人最多只能领一次红包
测试用的脚本 test.lua:
-- 剩余红包个数
local rpRemainedKey = KEYS[1];
-- 已领红包 hash
local userAwardedKey = KEYS[2];
-- 领取红包的用户id
local userID = ARGV[1];
-- 领取红包实际多少钱
local money = ARGV[2];
-- 判断用户是否已经领过红包了
local userAwarded = redis.call("HGET", userAwardedKey, userID);
if userAwarded then
return -1;
end
-- 判断红包剩余数
local remained = redis.call("GET", rpRemainedKey);
local iRemained = tonumber(remained);
if iRemained <= 0 then
return -2;
end
-- 红包数 减一
iRemained = redis.call("DECR", rpRemainedKey);
redis.call("HSET", userAwardedKey, userID, money);
-- 返回还剩余多少个红包
return iRemained
请注意redis-cli eval时的格式: KEYS 和 ARGV是通过","来分隔的
$ redis-cli -a '123456' --eval test.lua rp-remained user-awarded , zhangsan 88
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
(error) ERR Error running script (call to f_359ab1f8ebcc70ab7f608a16e245baecf9b6bcdf): @user_script:22: user_script:22: attempt to compare nil with number
报错说是: 22行的if iRemained <= 0 then
将nil和number比较, 说明GET rp-remained
是nil
, 也就是没有在redis-server中设置红包个数, 通过SET rp-remained 5
设置5个红包再执行上述命令:
$ redis-cli -a '123456' --eval test.lua rp-remained user-awarded , zhangsan 88
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
(integer) 4
说明发红包成功, 目前红包剩余4个.
再执行一次, 发现用户已领红包, 直接返回-1
$ redis-cli -a '123456' --eval test.lua rp-remained user-awarded , zhangsan 88
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
(integer) -1
关于redis使用lua debugger的更多信息, 看这里: Redis Lua scripts debugger
注意下面的命令加了–ldb选项
我们这里来演示下:
$ redis-cli -a '123456' --ldb --eval test.lua rp-remained user-awarded , zhangsan 88
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
Lua debugging session started, please use:
quit -- End the session.
restart -- Restart the script in debug mode again.
help -- Show Lua script debugging commands.
* Stopped at 2, stop reason = step over
-> 2 local rpRemainedKey = KEYS[1];
lua debugger> s
* Stopped at 5, stop reason = step over
-> 5 local userAwardedKey = KEYS[2];
lua debugger> s
* Stopped at 8, stop reason = step over
-> 8 local userID = ARGV[1];
lua debugger> s
* Stopped at 11, stop reason = step over
-> 11 local money = ARGV[2];
lua debugger> s
* Stopped at 14, stop reason = step over
-> 14 local userAwarded = redis.call("HGET", userAwardedKey, userID);
lua debugger> p money
<value> "88"
lua debugger> s
<redis> HGET user-awarded zhangsan
<reply> "88"
* Stopped at 15, stop reason = step over
-> 15 if userAwarded then
lua debugger> p userAwarded
<value> "88"
lua debugger> s
* Stopped at 16, stop reason = step over
-> 16 return -1;
lua debugger> s
(integer) -1
(Lua debugging session ended -- dataset changes rolled back)
127.0.0.1:6379>
熟悉gdb或者dlv的同学应该对上述调试界面感觉非常亲切, 这里直接体验下效果更好.
更多调试指令, 通过在 lua debugger>
下输入help来查看.
先说下结论: lua中只有false
和nil
是假值, 其他都是真值, 测试如下:
test.lua:
function judge(v)
if v then
print(v, "is true value")
else
print(v, "is false value")
end
end
judge(0)
judge(1)
judge(-1)
judge(nil)
judge(false)
judge(true)
judge({})
judge("")
judge("abc")
运行看看:
$ lua test.lua
0 is true value
1 is true value
-1 is true value
nil is false value
false is false value
true is true value
table: 0x87c420 is true value
is true value
abc is true value
上述脚本我们在自己开发机上调试好了, 最终还是要用其他语言来调的, 这里以go语言为例子:
跑go代码之前先将数据复原:
127.0.0.1:6379> keys *
1) "rp-remained"
127.0.0.1:6379> get "rp-remained"
"5"
main.go:
package main
import (
"context"
"fmt"
"log"
"sync"
"github.com/go-redis/redis/v8"
)
var (
ctx = context.Background()
rc *redis.Client
)
const rpLuaScript = `
-- 剩余红包个数
local rpRemainedKey = KEYS[1];
-- 已领红包 hash
local userAwardedKey = KEYS[2];
-- 领取红包的用户id
local userID = ARGV[1];
-- 领取红包实际多少钱
local money = ARGV[2];
-- 判断用户是否已经领过红包了
local userAwarded = redis.call("HGET", userAwardedKey, userID);
if userAwarded then
return -1;
end
-- 判断红包剩余数
local remained = redis.call("GET", rpRemainedKey);
local iRemained = tonumber(remained);
if iRemained <= 0 then
return -2;
end
-- 红包数 减一
iRemained = redis.call("DECR", rpRemainedKey);
redis.call("HSET", userAwardedKey, userID, money);
-- 返回还剩余多少个红包
return iRemained
`
func main() {
InitRedisCli()
var wg sync.WaitGroup
for i := 0; i < 10000; i++ {
userID := fmt.Sprintf("zhangsan:%d", i%10) // zhangsan0 - zhangsan9 每个人分别请求100次, 共1000次
money := i + 88
wg.Add(1)
go func() {
defer wg.Done()
cmd := rc.Eval(ctx, rpLuaScript, []string{"rp-remained", "user-awarded"}, userID, money)
remained, err := cmd.Int() // 因为lua脚本中return的是int, 所以这里转为int
if err != nil {
log.Printf("userID:%v, money:%v, err:%\n", userID, money, err)
return
}
if remained >= 0 {
log.Printf("success: userID:%v, money:%v, after get rp, remained:%v\n", userID, money, remained)
} else {
log.Printf("failed: userID:%v, money:%v, after get rp, remained:%v\n", userID, money, remained)
}
}()
}
wg.Wait()
}
func InitRedisCli() {
rc = redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "123456", // no password set
})
if err := rc.Ping(ctx).Err(); err != nil {
panic(err)
}
}
实验步骤:
go run main.go 2>&1 | tee main.log
127.0.0.1:6379> get "rp-remained"
"0"
127.0.0.1:6379> hgetall "user-awarded"
1) "zhangsan:4"
2) "92"
3) "zhangsan:5"
4) "93"
5) "zhangsan:7"
6) "115"
7) "zhangsan:9"
8) "137"
9) "zhangsan:2"
10) "90"
127.0.0.1:6379>
$ grep success main.log
2022/01/26 16:46:33 success: userID:zhangsan:4, money:92, after get rp, remained:4
2022/01/26 16:46:33 success: userID:zhangsan:5, money:93, after get rp, remained:3
2022/01/26 16:46:33 success: userID:zhangsan:7, money:115, after get rp, remained:2
2022/01/26 16:46:33 success: userID:zhangsan:9, money:137, after get rp, remained:1
2022/01/26 16:46:33 success: userID:zhangsan:2, money:90, after get rp, remained:0