一、背景
在应用 redis
的过程中,发现有些时候须要 原子性
去操作 redis 命令,而 redis 的 lua
脚本正好能够实现这一性能。比方: 扣减库存操作、限流操作等等。
redis 的 pipelining
尽管也能够一次执行一组命令,然而如果在这一组命令的执行过程中,须要依据上一步执行的后果做一些判断,则无奈实现。
二、应用 lua 脚本
Redis 中应用的是 Lua 5.1
的脚本标准,同时咱们编写的脚本的时候,不须要定义 Lua 函数。同时也不能应用全局变量等等。
1、lua 脚本的格局和注意事项
1、格局
EVAL script numkeys key [key …] arg [arg …]
127.0.0.1:6379> eval "return {KEYS[1],ARGV[1],ARGV[2]}" 1 key1 arg1 arg2
1) "key1"
2) "arg1"
3) "arg2"
127.0.0.1:6379>
2、注意事项
Lua
脚本中的 redis 操作的 key 最好都是通过 KEYS
来传递,而不要写死。否则在 Redis Cluster 的状况下可能有问题.
1、好的写法
127.0.0.1:6379> eval "return redis.call('set',KEYS[1],'zhangsan')" 1 username
OK
127.0.0.1:6379> get username
"zhangsan"
redis 命令操作的 key 是通过 KEYS 获取的。
2、差的写法
127.0.0.1:6379> eval "return redis.call('set','username','zhangsan')" 0
OK
127.0.0.1:6379> get username
"zhangsan"
redis 命令操作的 key 是间接写死的。
2、将脚本加载到 redis 中
需要: 此处定义一个 lua 脚本,将输出的参数的值 + 1 返回。
留神:
当咱们把 lua 脚本
加载到 redis 中,这个脚本并不会马上执行,而是会缓存起来,并且返回 sha1
校验和,前期咱们能够通过 EVALSHA
来执行这个脚本。
此处咱们记住这个脚本加载后返回的 hash 值,在下一步执行的时候须要用到。
127.0.0.1:6379> script load "return tonumber(KEYS[1])+1"
"ef424d378d47e7a8b725259cb717d90a4b12a0de"
127.0.0.1:6379>
3、执行 lua 脚本
1、通过 eval 执行
127.0.0.1:6379> eval "return tonumber(KEYS[1]) + 1" 1 100
(integer) 101
127.0.0.1:6379>
2、通过 evalsha 执行
ef424d378d47e7a8b725259cb717d90a4b12a0de
的值为上一步通过 script load
加载脚本后获取的。
127.0.0.1:6379> evalsha ef424d378d47e7a8b725259cb717d90a4b12a0de 1 100
(integer) 101
127.0.0.1:6379>
通过 evalsha
执行的益处是能够节俭带宽。如果咱们的 lua 脚本比拟长,程序在执行的时候将 lua 脚本发送到 redis 服务器则可能消耗的带宽多,如果发送的是 hash 值的话,则消耗的带宽少。
4、判断脚本是否在 redis 服务器缓存中
127.0.0.1:6379> script load "return tonumber(KEYS[1])+1"
"ef424d378d47e7a8b725259cb717d90a4b12a0de"
127.0.0.1:6379> script exists ef424d378d47e7a8b725259cb717d90a4b12a0de
1) (integer) 1
127.0.0.1:6379> script exists not-exists-sha1
1) (integer) 0
127.0.0.1:6379>
5、清空服务器上的脚本缓存
留神:
咱们无奈革除某一个脚本的缓存,只能够分明所有的缓存,个别状况下没有必要分明,因为即便有大量的脚本也不会太占用服务器内存。
127.0.0.1:6379> script load "return tonumber(KEYS[1])+1"
"ef424d378d47e7a8b725259cb717d90a4b12a0de"
127.0.0.1:6379> script exists ef424d378d47e7a8b725259cb717d90a4b12a0de
1) (integer) 1
127.0.0.1:6379> script flush
OK
127.0.0.1:6379> script exists ef424d378d47e7a8b725259cb717d90a4b12a0de
1) (integer) 0
6、杀死正在运行的脚本
127.0.0.1:6379> script kill
留神:
- 该命令只能够杀死正在运行的
只读脚本
。 - 对于批改了数据的脚本,无奈应用此命令杀死,只能应用
shutdown nosave
命令。 - 脚本执行的
默认超时工夫
为5 分钟
,能够通过redis.conf
配置文件的lua-time-limit
配置项批改。 - 脚本即便达到了超时工夫,也不会进行执行,因为这违反了 lua 脚本的原子性。
三、lua 和 redis 数据类型转换
Lua
的数据类型和 Redis
的数据类型存在一对一的转换关系,如果将 Redis 类型转换成 Lua 类型,而后在转换成 Redis 类型,那么后果和初试值是统一的。
1、类型转换
Redis to Lua conversion table.
- Redis integer reply -> Lua number
- Redis bulk reply -> Lua string
- Redis multi bulk reply -> Lua table (may have other Redis data types nested)
- Redis status reply -> Lua table with a single
ok
field containing the status - Redis error reply -> Lua table with a single
err
field containing the error - Redis Nil bulk reply and Nil multi bulk reply -> Lua false boolean type
Lua to Redis conversion table.
- Lua number -> Redis integer reply (the number is converted into an integer)
- Lua string -> Redis bulk reply
- Lua table (array) -> Redis multi bulk reply (truncated to the first nil inside the Lua array if any)
- Lua table with a single
ok
field -> Redis status reply - Lua table with a single
err
field -> Redis error reply - Lua boolean false -> Redis Nil bulk reply.
2、额定的转换规则
- Lua 的布尔类型,Lua 的 True 会转换成 Redis 的 1
3、3 个重要规定
1. 数字类型
在 Lua 中,只有一个 number
类型,整数和浮点数之间没有区别,如果咱们在 Lua 中返回一个浮点数,理论返回的是一个整数,如果要返回浮点数,须要以字符串的形式返回。
127.0.0.1:6379> eval "return 3.98" 0
(integer) 3
127.0.0.1:6379> eval "return'3.98'"0"3.98"
2. lua 数组存在 nil
当 Redis 将 Lua 数组转换为 Redis 协定时,如果遇到 nil,则转换会进行。即 nil 后的值都不会返回。
127.0.0.1:6379> eval "return {1,2,'data',nil,'can not return value','vv'}" 0
1) (integer) 1
2) (integer) 2
3) "data"
127.0.0.1:6379>
3. Lua 的 Table 类型蕴含建和值
呈现这种状况返回的 redis 的是一个空数组
127.0.0.1:6379> eval "return {key1 ='value1',key2='value2'}" 0
(empty array)
127.0.0.1:6379>
四、lua 脚本中输入日志
这个个别调试咱们的脚本的时候比拟有用。
redis.log(loglevel,message)
loglevel 的取值范畴:
redis.LOG_DEBUG
redis.LOG_VERBOSE
redis.LOG_NOTICE
redis.LOG_WARNING
举例:
五、一个简略限流的案例
1、需要
在 1s 之内,办法最大的并发只能是 5。
1s 和 5 当作参数传递。
2、实现步骤
1、编写 lua 脚本
-- 输入用户传递进来的参数
for i, v in pairs(KEYS) do
redis.log(redis.LOG_NOTICE, "limit: key" .. i .. "=" .. v)
end
for i, v in pairs(ARGV) do
redis.log(redis.LOG_NOTICE, "limit: argv" .. i .. "=" .. v)
end
-- 限流的 key
local limitKey = tostring(KEYS[1])
-- 限流的次数
local limit = tonumber(ARGV[1])
-- 多长时间过期
local expireMs = tonumber(ARGV[2])
-- 以后曾经执行的次数
local current = tonumber(redis.call('get', limitKey) or '0')
-- 设置一个断点
redis.breakpoint()
redis.log(redis.LOG_NOTICE, "limit key:" .. tostring(limitKey) .. "在[" .. tostring(expireMs) .. "]ms 内曾经拜访了" .. tostring(current) .. "次, 最多能够拜访:" .. limit .. "次")
-- 限流了
if (current + 1 > limit) then
return {true}
end
-- 未达到拜访限度
-- 拜访次数 +1
redis.call("incrby", limitKey, "1")
if (current == 0) then
-- 设置过期工夫
redis.call("pexpire", limitKey, expireMs)
end
return {false}
2、程序中执行 lua 脚本
残缺代码: https://gitee.com/huan1993/spring-cloud-parent/tree/master/springboot/springboot-redis-lua
六、lua 脚本的 debug
当咱们编写好了 lua 脚本后,如果在执行的过程中产生了谬误,那么咱们如何该如何解决呢?此处咱们来理解下如何 debug lua 脚本。
1、lua 脚本中的几个小命令
在 脚本中打一个断点
redis.breakpoint()
2、断点调试
1、执行命令
redis-cli --ldb --eval limit.lua invoked , 1 1000
limit.lua 须要 debug 的 lua 文件
invoked 为传递到 lua 脚本中 KEYS 的值
1 和 1000 为传递到 lua 脚本中 ARGV 的值
, 宰割 出 KEYS 和 ARGV 的值
2、一些 debug 指令
help
:列出可用的 debug 指令s
或n
:运行到以后行并进行(此时以后行还未执行)c
:运行到下个断点,即运行到 lua 脚本中存在redis.breakpoint()
办法的中央list
:列出以后行四周的一些源码p
:打印出所有的 local 变量的值p <var>
:打印具体的某个 local 变量的值-
r
:执行 redis 命令-- eg: r set key value r get key
3、debug 运行后果
七、参考文档
- https://redis.io/topics/ldb
- https://redis.io/commands/eval