文章目录
- 四、尾声
一、前言
多个命令全部成功或者全部失败,怎么实现?
可以使用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语言,它们是可填可不填的。
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 脚本,示例:
如果 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文件
注意: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。
tonumber 是一个函数,就是将变量类型转换为数字类型,然后才可以用来作为数字比较
local num=redis.call(“incr”,KEYS[1]) 放到lua脚本开始,用来记录lua脚本的访问次数,记录在同一个KEYS[1]的value 里面,value第一次从 0 变成 1,第二次从1 变成 2,这样就巧妙的用 value 来记录访问次数了,然后和被限制的访问次数 tonumber(ARGV[2] 比较
3.2 案例:缓存Lua脚本和自乘案例
3.2.1 通过摘要调用lua脚本
在lua脚本比较长的情况下,如果每次调用脚本都需要将整个脚本传入到redis服务端,会产生比较大的网络开销。为了解决这个问题,Redis可以缓存Lua脚本并生成 SHA1 摘要码,后面可以直接通过摘要码来执行 Lua 脚本。
3.2.2 自乘案例
自乘案例(lua脚本可以执行一些 redis 无法直接通过命令执行的操作)
lua脚本可以执行一些 redis 无法直接通过命令执行的操作,因为lua脚本可以同时执行 lua语法 和 redis 命令,lua语法 里面有乘法
也可以通过摘要调用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
3.3.2 lua脚本执行死循环,lua脚本中存在redis set
四、尾声
小结一下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命令原子性,完成了。