Redis_05_Lua脚本实现多条Redis命令原子性

Posted 毛奇志

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Redis_05_Lua脚本实现多条Redis命令原子性相关的知识,希望对你有一定的参考价值。

文章目录

一、前言

多个命令全部成功或者全部失败,怎么实现?
可以使用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

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

如果 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

注意: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] 比较

# 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

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

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

也可以通过摘要调用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

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

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


四、尾声

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

以上是关于Redis_05_Lua脚本实现多条Redis命令原子性的主要内容,如果未能解决你的问题,请参考以下文章

redis与lua

Redis学习笔记—Redis与Lua

Lua脚本在Redis事务中的应用实践

redis 简单整理——Lua[十一]

Redis:18---常用功能之(Lua脚本)

Redis + LUA 脚本实现分布式限流