• 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
    
    • 1
    • 2

    在这里插入图片描述

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    在这里插入图片描述

    如果 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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    在这里插入图片描述

    注意: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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    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
    
    • 1
    • 2
    • 3
    • 4

    在这里插入图片描述

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

    3.2.1 通过摘要调用lua脚本

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

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

    在这里插入图片描述

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    在这里插入图片描述

    也可以通过摘要调用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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    在这里插入图片描述

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    在这里插入图片描述
    在这里插入图片描述

    四、尾声

    小结一下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命令原子性,完成了。

  • 相关阅读:
    杰理之mic 初始化及数据出口的 API【篇】
    计算器的简化版
    查看mysql死锁语法
    Pytorch总结八之深度学习计算(1)模型构造,参数访问、初始化和共享
    信号发送与处理-上
    金融时间序列预测方法合集:CNN、LSTM、随机森林、ARMA预测股票价格(适用于时序问题)、相似度计算、各类评判指标绘图(数学建模科研适用)
    线程安全问题(模拟取钱案例)
    计算机专业毕业设计项目推荐02-个人医疗系统(Java+原生Js+Mysql)
    K8S(二):Pod入门基本概念,网络和存储共享,Infra/Pause容器
    1的取反为什么是-2
  • 原文地址:https://blog.csdn.net/qq_36963950/article/details/126800403