Redis_07_Lua脚本实现多条Redis命令原子性
Posted 毛奇志
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Redis_07_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脚本相关知识点,如下:
知识点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命令原子性,完成了。
以上是关于Redis_07_Lua脚本实现多条Redis命令原子性的主要内容,如果未能解决你的问题,请参考以下文章