一、背景

在应用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 arg21) "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 usernameOK127.0.0.1:6379> get username"zhangsan"

redis命令操作的key是通过KEYS获取的。

2、差的写法

127.0.0.1:6379> eval "return redis.call('set','username','zhangsan')" 0OK127.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) 101127.0.0.1:6379>

2、通过evalsha执行

ef424d378d47e7a8b725259cb717d90a4b12a0de的值为上一步通过 script load加载脚本后获取的。

127.0.0.1:6379> evalsha ef424d378d47e7a8b725259cb717d90a4b12a0de 1 100(integer) 101127.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 ef424d378d47e7a8b725259cb717d90a4b12a0de1) (integer) 1127.0.0.1:6379> script exists not-exists-sha11) (integer) 0127.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 ef424d378d47e7a8b725259cb717d90a4b12a0de1) (integer) 1127.0.0.1:6379> script flushOK127.0.0.1:6379> script exists ef424d378d47e7a8b725259cb717d90a4b12a0de1) (integer) 0

6、杀死正在运行的脚本

127.0.0.1:6379> script kill

留神:

  1. 该命令只能够杀死正在运行的 只读脚本
  2. 对于批改了数据的脚本,无奈应用此命令杀死,只能应用 shutdown nosave命令。
  3. 脚本执行的默认超时工夫5分钟,能够通过redis.conf配置文件的lua-time-limit配置项批改。
  4. 脚本即便达到了超时工夫,也不会进行执行,因为这违反了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、额定的转换规则

  1. Lua的布尔类型,Lua的True会转换成Redis的1

3、3个重要规定

1. 数字类型

在Lua中,只有一个number类型,整数和浮点数之间没有区别,如果咱们在Lua中返回一个浮点数,理论返回的是一个整数,如果要返回浮点数,须要以字符串的形式返回。

127.0.0.1:6379> eval "return 3.98" 0(integer) 3127.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'}" 01) (integer) 12) (integer) 23) "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)endfor i, v in pairs(ARGV) do    redis.log(redis.LOG_NOTICE, "limit: argv" .. i .. " = " .. v)end-- 限流的keylocal 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-- 未达到拜访限度-- 拜访次数+1redis.call("incrby", limitKey, "1")if (current == 0) then    -- 设置过期工夫    redis.call("pexpire", limitKey, expireMs)endreturn { 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 1000limit.lua 须要debug的lua文件invoked 为传递到 lua 脚本中 KEYS 的值1 和 1000 为传递到 lua 脚本中 ARGV 的值, 宰割 出 KEYS 和 ARGV 的值

2、一些debug指令

  • help: 列出可用的debug指令
  • sn: 运行到以后行并进行 (此时以后行还未执行)
  • c:运行到下个断点,即运行到lua脚本中存在 redis.breakpoint()办法的中央
  • list:列出以后行四周的一些源码
  • p:打印出所有的 local 变量的值
  • p <var>:打印具体的某个 local 变量的值
  • r:执行 redis 命令

    -- eg:r set key value r get key

3、debug运行后果

七、参考文档

  1. https://redis.io/topics/ldb
  2. https://redis.io/commands/eval