Lua脚本

Lua是一种轻量级脚本语言,它是用C语言编写的,跟数据的存储过程有点类似。

使用Lua脚本来执行Redis命令的好处:
1、 一次发送多个命令,减少网络开销。
2、 Redis会将整个脚本作为一个整体执行,不会被其他请求打断,保持原子性。
3、 对于复杂的组合命令,我们可以放在文件中,可以实现命令复用。

在Redis中调用Lua脚本

使用eval方法,语法格式:
redis> eval lua-script key-num [keyl key2 key3 ....] [value 1 value2 value3 ....]

  • eval代表执行Lua语言的命令。
  • lua-script代表Lua语言脚本内容。
  • key-num表示参数中有多少个key,需要注意的是Redis中key是从1开始的,如果没有key的参数,那么写0。
  • [keyl key2 key3…]是key作为参数传递给Lua语言,也可以不填,但是需要和key-num的个数对应起来。
  • [valuel value2 value3…」这些参数传递给Lua语言,它们是可填可不填的。

示例,返回一个字符串,0个参数:

redis> eval ”return 'Hello World"' 0

实际上,Lua脚本在Redis里面真正的用途是用来执行Redis命令。

在Lua脚本中调用Redis命令

命令格式

使用 redis.call(command, key [paraml, param2...])进行操作。语法格式:

redis. call (command, key [param l,param2...])
  • command 是命令,包括 set、get> del 等。
  • key是被操作的键。
  • paraml,param2...代表给 key 的参数。

简单的案例,让Lua脚本执行set snail 2673 (Redis客户端执行):

eval return redis.call('set', 'snail','2573')  0

这种方式是写死值的,当然也可以用传参的方式:

eval return redis.call('set' .KEYS[1],ARGV[1])" 1 snail xiaoming

如果KEY和ARGV有多个,继续往后面加就是了。
在redis-cli中直接写Lua脚本不够方便,也不能实现编辑和复用,通常我们会把Lua 脚本放在文件里面,然后执行这个文件。

Lua脚本文件

创建Lua脚本文件:

cd /usr/local/soft/redis-6.0.9/src
vim snail.lua 

Lua脚本内容,先赋值,再取值:

redis. call('set', 'snail',lua1')
return redis.call('get','snail')

调用脚本文件:

cd /usr/local/soft/redis-6.0.9/src
redis-cli --eval snail.lua 0

场景案例:对IP进行限流

需求:每个用户在X秒内只能访问Y次。
设计思路:
1、首先是数据类型。用String的key记录IP,用value记录访问次数。几秒钟和几次要用参数动态传进去。
拿到IP以后,对IP+1。如果是第一次访问,对key设置过期时间(参数1)。否则判断次数,超过限定的次数(参数2),返回0。如果没有超过次数则返回1。超过时间, key过期之后,可以再次访问。

KEY[1]是IP, ARGV[1]是过期时间X, ARGV[2]是限制访问的次数Y。

-- ip_limit.lua
-- ip限流,对某个IP频率进行限制,6秒钟访问10次
local num=redis.call('incr',KEYS[1])
if tonumber(num)== 1 then
        redis.call('expire',KEYS[1],ARGV[1])
        return 1
      elseif tonumber(num)>tonumber(ARGV[2]) then
return 0
else
return 1
end

6秒钟内限制访问10次,调用测试(连续调用10次):redis-cli --eval ipjimit.lua app:ip:limit: 192.168.8.111,6 10

  • app:ip:limit:192.168.8.111是key值,后面是参数值,中间要加上一个空格和 —个逗号,再加上一个空格。
    即:redis-cli -eval [lua 脚本][key...]空格,空格[args...]

  • 多个参数之间用空格分割。

缓存Lua脚本

为什么要缓存?

在Lua脚本比较长的情况下,如果每次调用脚本都需要把整个脚本传给Redis服务 端,会产生比较大的网络开销。为了解决这个问题,Redis可以缓存Lua脚本并生成SHA1 摘要码,后面可以直接通过摘要码来执行Lua脚本。

如何缓存?

这里面涉及到两个命令,首先是在服务端缓存山a脚本生成一个摘要码,用script load命令。

script load "return ‘Hello World’“

第二个命令是通过摘要码执行缓存的脚本:

evalsha ”470877a599ac74fbfda41caa908de682c5fc7d4b” 0

自乘案例

Redis有incrby这样的自增命令,但是没有自乘,比如乘以3,乘以5。

set num 2

我们可以写一个自乘的运算,让它乘以后面的参数:

local curVal = redis.call(''get", KEYS[1]) 
if curVal == false then
  curVal = 0
else
  curVal = tonumber(curVal)
end
curVal = curVal * tonumber(ARGV[ 1 ]) 
redis.call("set", KEYS[1], curVal) 
return curVal

把这个脚本变成单行,语句之间使用分号隔开:

local curVal = redis.call("getL KEYS[1]); if curVal == false then curVal = 0 else curVal = tonumber(curVal) end; curVal = curVal * tonumber(ARGV[1]); redis.call("set", KEYS[1], curVal); return curVal

script load命令(Redis客户端执行)

script load local curVal = redis.call("get", KEYS[1]); if curVal == false then curVal = 0 else curVal = tonumber(curVal) end; curVal = curVal * tonumber(ARGV[1]); redis.call("set", KEYS[1], curVal); return curVar

"be4f93d8a5379e5e5b768a74e77c8a4eb0434441"

调用:

evalsha be4f93d8a5379e5e5b768a74e77c8a4eb0434441 1 num 6

使用Lua脚本需要注意一个问题---脚本超时。

脚本超时

Redis的指令执行本身是单线程的,这个线程还要执行客户端的Lua脚本,如果Lua 脚本执行超时或者陷入了死循环,是不是没有办法为客户端提供服务了呢?

eval  'while(true) do end' 0

还真是的。它会导致其他的命令都会进入等待状态。
当然,这种小问题,antirez在设计的时候引入lua脚本的时候就考虑到了。
首先,脚本执行有一个超时时间,默认为5秒钟。

lua-time-limit 5000

超过5秒钟,其他客户端的命令不会等待,而是直接会返回"BUSY"错误。

但是这样也不行,不能一直拒绝其他客户端的命令执行吧。在提示里面我们也看到了, 有两个命令可以使用,第一个是script kill,中止脚本的执行。

script kill

但是需要注意:并不是所有的lua脚本执行都可以kill。如果当前执行的Lua脚本对 Redis的数据进行了修改(SET、DEL等),那么通过script kill命令是不能终止脚本运 行的。

eval  "redis.call('set','snail','666') while true do end" 0

这时候执行script kill会返回UNKILLABLE错误。为什么要这么设计?为什么包含修改的脚本不能中断?因为要保证脚本运行的原子性。如果脚本执行了一部分被终止, 那就违背了脚本原子性的目标。

遇到这种情况,只能通过shutdown nosave命令,直接把Redis服务停掉。

正常关机是 shutdown。 shutdown nosave 和 shutdown 的区别在于 shutdown nosave不会进行持久化操作,意味着发生在上一次快照后的数据库修改都会丢失。

总结:如果我们有一些特殊的需求,可以用Lua来实现,但是要注意那些耗时的操作。

posted @ 2021-01-22 15:46  snail灬  阅读(1214)  评论(0编辑  收藏  举报