文章目录

  • 一、前言
  • 二、Lua脚本具体操作
  • 2.1 Lua脚本可以保证原子性
  • 2.2 Redis中执行Lua脚本
  • 2.3 在Lua脚本中执行Redis命令
  • 2.4 将lua脚本放到文件里
  • 三、Lua脚本使用
  • 3.1 案例:对IP进行限流
  • 3.2 案例:缓存Lua脚本和自乘案例
  • 3.2.1 通过摘要调用lua脚本
  • 3.2.2 自乘案例
  • 3.3 案例:脚本超时
  • 3.3.1 lua脚本执行死循环,lua脚本中没有redis set
  • 3.3.2 lua脚本执行死循环,lua脚本中存在redis set
  • 四、尾声


一、前言

多个命令全部成功或者全部失败,怎么实现?
可以使用lua脚本,方案是:redis客户端里面写 lua脚本,lua脚本中执行多条命令,然后在redis客户端执行这个 lua脚本。

二、Lua脚本具体操作

2.1 Lua脚本可以保证原子性

Lua脚本 为什么用Lua脚本?
1、批量执行命令
2、原子性
3、操作集合的复用

lua脚本使用方法: redis客户端 中执行lua脚本,lua脚本中 执行 redis 命令
解释:为什么不直接 redis 客户端执行 redis 命令,要中间加一个 lua 脚本,就是为了要保证原子性

2.2 Redis中执行Lua脚本

Redis中执行Lua脚本,示例:

redis> eval lua-script key-num [key1 key2 key3 …] [value1 value2 value3 …]

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

# 直接在 redis-cli 中调用这个 lua 脚本
eval "return 'Hello World'" 0

redis怎么执行lua脚本 redis集群执行lua脚本_redis怎么执行lua脚本

2.3 在Lua脚本中执行Redis命令

redis.call(command, key [param1,param2…])

对于上面命令的解释:
command是命令,包括set、get、del等。
key是被操作的键。
param1,param2…代表给key的参数。

上面是直接在 redis-cli 中调用这个 lua 脚本,现在我们先在 lua 脚本中调用redis命令,然后再在 redis-cli 中调用这个 lua 脚本,示例:

# 先在 lua 脚本中调用redis命令,然后再在 redis-cli 中调用这个 lua 脚本
eval "return redis.call('set','qingshan','2673')" 0
get qingshan
eval "return redis.call('get','qingshan')" 0

# 先在 lua 脚本中调用redis命令,然后再在 redis-cli 中调用这个 lua 脚本(传参数的方式实现)
eval "return redis.call('set',KEYS[1],ARGV[1])" 1 qingshan miaomiaomiao
get qingshan
eval "return redis.call('get','qingshan')" 0
eval "return redis.call('get',KEYS[1])" 1 qingshan

redis怎么执行lua脚本 redis集群执行lua脚本_redis怎么执行lua脚本_02

如果 KEY 和 ARGV 有多个,继续往后面加就是了。

在 redis-cli 中直接写 Lua 脚本不够方便,也不能实现编辑和复用,通常我们会把Lua脚本放到文件里面,然后执行这个文件。

2.4 将lua脚本放到文件里

编写操作Redis命令:
redis.call(command, key [param1,param2…])

调用lua脚本:
redis-cli --eval 脚本名称 参数个数 参数1 参数2……

步骤1:创建lua脚本文件,文件格式为 xxx.lua
步骤2:编写lua脚本文件,里面直接写 lua语法 或者 redis.call
步骤3:./redis-cli --raw 打开redis-cli客户端,然后调用linux上编写的lua文件

# 创建lua脚本文件,文件格式为 xxx.lua 
cd /root/redis-6.0.9/src
vi xxx.lua
# 编写lua脚本文件,里面直接写 redis.call
redis.call('set','qingshan','lua666')
return redis.call('get','qingshan')
# redis-cli中调用lua脚本文件
./redis-cli --eval xxx.lua 0

redis怎么执行lua脚本 redis集群执行lua脚本_开发语言_03

注意:xxx.lua 放在 redis-cli 同级目录下,所以才可以直接 ./redis-cli --eval xxx.lua 0 调用到这个 xxx.lua 否则要指定 xxx.lua 所在目录。

三、Lua脚本使用

3.1 案例:对IP进行限流

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

[root@localhost src]# vi ip_limit.lua
[root@localhost src]# cat ip_limit.lua 
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

tonumber 是一个函数,就是将变量类型转换为数字类型,然后才可以用来作为数字比较
local num=redis.call(“incr”,KEYS[1]) 放到lua脚本开始,用来记录lua脚本的访问次数,记录在同一个KEYS[1]的value 里面,value第一次从 0 变成 1,第二次从1 变成 2,这样就巧妙的用 value 来记录访问次数了,然后和被限制的访问次数 tonumber(ARGV[2] 比较

redis怎么执行lua脚本 redis集群执行lua脚本_Lua_04

# 60秒访问10次(ip_limit.lua 有没有双引号都可以,但是 key 后面必须 空格+英文逗号+空格,然后接实参)
./redis-cli --eval ip_limit.lua app:ip:limit:192.168.8.111 , 60 10
或者 
./redis-cli --eval "ip_limit.lua" app:ip:limit:192.168.8.111 , 60 10

redis怎么执行lua脚本 redis集群执行lua脚本_redis_05

3.2 案例:缓存Lua脚本和自乘案例

3.2.1 通过摘要调用lua脚本

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

script load 脚本内容
evalsha "摘要值" 参数个数 参数1 参数2……
script load "return 'Hello World'"
evalsha "摘要值" 0

redis怎么执行lua脚本 redis集群执行lua脚本_redis怎么执行lua脚本_06

3.2.2 自乘案例

自乘案例(lua脚本可以执行一些 redis 无法直接通过命令执行的操作)

lua脚本可以执行一些 redis 无法直接通过命令执行的操作,因为lua脚本可以同时执行 lua语法 和 redis 命令,lua语法 里面有乘法

# 编写lua脚本(lua脚本里面执行redis命令),然后redis-cli中调用lua脚本
vi multi.lua
cat multi.lua
local curVal = redis.call("get", KEYS[1])
if curVal == false then
  curVal = 1
else 
  curVal = tonumber(curVal)
end
curVal = curVal * tonumber(ARGV[1])
redis.call("set", KEYS[1], curVal)
return curVal
./redis-cli --eval multi.lua key7 , 3
./redis-cli --eval multi.lua key7 , 3
./redis-cli --eval multi.lua key7 , 3

redis怎么执行lua脚本 redis集群执行lua脚本_redis怎么执行lua脚本_07

也可以通过摘要调用lua脚本,变成一行
local curVal = redis.call(“get”, KEYS[1]); if curVal == false then curVal = 1 else curVal = tonumber(curVal) end; curVal = curVal * tonumber(ARGV[1]); redis.call(“set”, KEYS[1], curVal); return curVal

script load ‘local curVal = redis.call(“get”, KEYS[1]); if curVal == false then curVal = 1 else curVal = tonumber(curVal) end; curVal = curVal * tonumber(ARGV[1]); redis.call(“set”, KEYS[1], curVal); return curVal’

evalsha e566ff330d1fb0495bc623dcd930dc3fd0dcbf5b 1 num 6

redis怎么执行lua脚本 redis集群执行lua脚本_Lua_08

3.3 案例:脚本超时

脚本超时两种情况:
(1) lua脚本执行死循环,lua脚本中没有redis set,另一个redis-cli使用 script kill 回滚
(2) lua脚本执行死循环,lua脚本中存在redis set,另一个redis-cli使用 shutdown nosave 回滚

3.3.1 lua脚本执行死循环,lua脚本中没有redis set

# 第一个redis-cli客户端lua脚本执行死循环(lua脚本中没有redis set)
./redis-cli --raw
eval "while(true) do end" 0

# 第二个redis-cli客户端执行script kill
./redis-cli --raw
get qingshan
script kill
get qingsha

redis怎么执行lua脚本 redis集群执行lua脚本_redis怎么执行lua脚本_09

3.3.2 lua脚本执行死循环,lua脚本中存在redis set

# 第一个redis-cli客户端lua脚本执行死循环(lua脚本中有redis set)
./redis-cli --raw
eval "redis.call('set','gupao','666') while(true) do end" 0

# 第二个redis-cli客户端执行shutdown nosave
./redis-cli --raw
get qingshan
script kill
shutdown nosave
exit

# 第二个redis-cli客户端重新进入还是不行
./redis-cli --raw
get qingshan 
exit

# 第二个redis-cli客户端杀死redis进程重启,然后再次进入,可以了
ps -ef|grep redis
kill -9 xxx
cd ..
./src/redis-server redis.conf
./redis-cli --raw
get qingshan

redis怎么执行lua脚本 redis集群执行lua脚本_开发语言_10


redis怎么执行lua脚本 redis集群执行lua脚本_redis怎么执行lua脚本_11

四、尾声

小结一下Lua脚本相关知识点,如下:

知识点1:lua脚本可以执行一些 redis 无法直接通过命令执行的操作,因为lua脚本中可以同时使用lua语言和redis.call,比如上面的乘法运算,就是通过lua语言实现的,单单通过redis.call无法实现。

知识点2:直接在命令行执行lua脚本很简单,如果通过 jedis lettuce redission 执行lua脚本,整个lua脚本比较大,造成较大的网络消耗,此时提供了一个 lua脚本摘要,只需要生成并执行这个摘要就好了。

知识点3:lua脚本保证原子性的原理相当于数据库的 库锁或表锁,就是 一个lua 脚本会锁住整个 redis-server ,其他所有 redis-client (包括命令行 jedis lettuce redission) 此时都无法的对 redis-server 发送命令 (证明方法:lua脚本写一个死循环,其他各种各样的redis客户端就连不上了,因为redis是单线程处理客户端请求)

知识点4:停止lua脚本死循环的两种方法
redis中使用lua脚本保证原子性,如果lua脚本死循环,所有的redis客户端都不允许操作了,相当于独占锁,所以是安全的,保证原子性,但是如何跳出lua脚本中的死循环呢?
(1) 如果lua脚本只有 读命令,可以直接关闭lua脚本,让其他客户端进来
(2) 如果lua脚本村子 写命令,必须执行 shutdown unsave 通过停止整个redis-server 来停止 lua脚本,此时lua脚本中的东西不会被保存 (除非rdb或者aof持久化了)

知识点5:分布式锁
redis分布式锁一定要使用lua脚本才能实现,因为分布式锁涉及多条redis命令,而加锁操作要求是原子的,但是一条条发送到 redis-server 无法保证整个加锁 set key value 是原子的,所以分布式锁一定需要 lua 脚本实现

知识点6:lua脚本回滚一定有一个类似 undo log日志的支持
如果lua脚本中出现redis语法错误,会回滚,lua脚本能够保证原子性,就一定有出错的时候的回滚机制,就一定有 undo log 回滚日志的支持

知识点7:lua脚本原子性造成性能影响
lua脚本保证原子性是相当于给redis加上了库锁,独占redis使用,不让别的redis-cli使用redis-server,但是如果lua脚本需要执行很长时间的话,别的redis-cli在这段时间内无法使用redis-server,造成性能影响。

Lua脚本实现多条Redis命令原子性,完成了。