• Redis入门完整教程:事务与Lua


    为了保证多条命令组合的原子性,Redis提供了简单的事务功能以及集
    成Lua脚本来解决这个问题。本节首先简单介绍Redis中事务的使用方法以及
    它的局限性,之后重点介绍Lua语言的基本使用方法,以及如何将Redis和
    Lua脚本进行集成,最后给出Redis管理Lua脚本的相关命令。

    3.4.1 事务
    熟悉关系型数据库的读者应该对事务比较了解,简单地说,事务表示一
    组动作,要么全部执行,要么全部不执行。例如在社交网站上用户A关注了
    用户B,那么需要在用户A的关注表中加入用户B,并且在用户B的粉丝表中
    添加用户A,这两个行为要么全部执行,要么全部不执行,否则会出现数据
    不一致的情况。
    Redis提供了简单的事务功能,将一组需要一起执行的命令放到multi和
    exec两个命令之间。multi命令代表事务开始,exec命令代表事务结束,它们
    之间的命令是原子顺序执行的,例如下面操作实现了上述用户关注问题。
    127.0.0.1:6379> multi
    OK
    127.0.0.1:6379> sadd user:a:follow user:b
    QUEUED
    127.0.0.1:6379> sadd user:b:fans user:a
    QUEUED
    可以看到sadd命令此时的返回结果是QUEUED,代表命令并没有真正执
    行,而是暂时保存在Redis中。如果此时另一个客户端执行sismember user:
    a:follow user:b返回结果应该为0。
    127.0.0.1:6379> sismember user:a:follow user:b
    (integer) 0
    只有当exec执行后,用户A关注用户B的行为才算完成,如下所示返回
    的两个结果对应sadd命令。
    127.0.0.1:6379> exec
    1) (integer) 1
    2) (integer) 1
    127.0.0.1:6379> sismember user:a:follow user:b
    (integer) 1

    如果要停止事务的执行,可以使用discard命令代替exec命令即可。
    127.0.0.1:6379> discard
    OK
    127.0.0.1:6379> sismember user:a:follow user:b
    (integer) 0
    如果事务中的命令出现错误,Redis的处理机制也不尽相同。
    1.命令错误
    例如下面操作错将set写成了sett,属于语法错误,会造成整个事务无法
    执行,key和counter的值未发生变化:
    127.0.0.1:6388> mget key counter
    1) "hello"
    2) "100"
    127.0.0.1:6388> multi
    OK
    127.0.0.1:6388> sett key world
    (error) ERR unknown command 'sett'
    127.0.0.1:6388> incr counter
    QUEUED
    127.0.0.1:6388> exec
    (error) EXECABORT Transaction discarded because of previous errors.
    127.0.0.1:6388> mget key counter
    1) "hello"
    2) "100"
    2.运行时错误
    例如用户B在添加粉丝列表时,误把sadd命令写成了zadd命令,这种就
    是运行时命令,因为语法是正确的:
    127.0.0.1:6379> multi
    OK
    127.0.0.1:6379> sadd user:a:follow user:b
    QUEUED
    127.0.0.1:6379> zadd user:b:fans 1 user:a
    QUEUED
    127.0.0.1:6379> exec
    1) (integer) 1
    2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
    127.0.0.1:6379> sismember user:a:follow user:b
    (integer) 1
    可以看到Redis并不支持回滚功能,sadd user:a:follow user:b命令已
    经执行成功,开发人员需要自己修复这类问题。
    有些应用场景需要在事务之前,确保事务中的key没有被其他客户端修
    改过,才执行事务,否则不执行(类似乐观锁)。Redis提供了watch命令来
    解决这类问题,表3-2展示了两个客户端执行命令的时序。

     

    可以看到“客户端-1”在执行multi之前执行了watch命令,“客户
    端-2”在“客户端-1”执行exec之前修改了key值,造成事务没有执行(exec结果
    为nil),整个代码如下所示:
    #T1 :客户端 1
    127.0.0.1:6379> set key "java"
    OK
    #T2 :客户端 1
    127.0.0.1:6379> watch key
    OK
    #T3 :客户端 1
    127.0.0.1:6379> multi
    OK
    #T4 :客户端 2
    127.0.0.1:6379> append key python
    (integer) 11
    #T5 :客户端 1
    127.0.0.1:6379> append key jedis
    QUEUED
    #T6 :客户端 1
    127.0.0.1:6379> exec
    (nil)
    #T7 :客户端 1
    127.0.0.1:6379> get key
    "javapython"
    Redis提供了简单的事务,之所以说它简单,主要是因为它不支持事务
    中的回滚特性,同时无法实现命令之间的逻辑关系计算,当然也体现了
    Redis的“keep it simple”的特性,下一小节介绍的Lua脚本同样可以实现事务
    的相关功能,但是功能要强大很多。 

    3.4.2 Lua用法简述
    Lua语言是在1993年由巴西一个大学研究小组发明,其设计目标是作为
    嵌入式程序移植到其他应用程序,它是由C语言实现的,虽然简单小巧但是
    功能强大,所以许多应用都选用它作为脚本语言,尤其是在游戏领域,例如
    大名鼎鼎的暴雪公司将Lua语言引入到“魔兽世界”这款游戏中,Rovio公司将
    Lua语言作为“愤怒的小鸟”这款火爆游戏的关卡升级引擎,Web服务器Nginx
    将Lua语言作为扩展,增强自身功能。Redis将Lua作为脚本语言可帮助开发
    者定制自己的Redis命令,在这之前,必须修改源码。在介绍如何在Redis中
    使用Lua脚本之前,有必要对Lua语言的使用做一个基本的介绍。
    1.数据类型及其逻辑处理
    Lua语言提供了如下几种数据类型:booleans(布尔)、numbers(数
    值)、strings(字符串)、tables(表格),和许多高级语言相比,相对简
    单。下面将结合例子对Lua的基本数据类型和逻辑处理进行说明。
    (1)字符串
    下面定义一个字符串类型的数据:
    local strings val = "world"
    其中,local代表val是一个局部变量,如果没有local代表是全局变量。
    print函数可以打印出变量的值,例如下面代码将打印world,其中"--"是Lua
    语言的注释。

    --  结果是 "world"
    print(hello)
    (2)数组
    在Lua中,如果要使用类似数组的功能,可以用tables类型,下面代码使
    用定义了一个tables类型的变量myArray,但和大多数编程语言不同的是,
    Lua的数组下标从1开始计算:
    local tables myArray = {"redis", "jedis", true, 88.0}
    --true
    print(myArray[3])
    如果想遍历这个数组,可以使用for和while,这些关键字和许多编程语
    言是一致的。
    (a)for
    下面代码会计算1到100的和,关键字for以end作为结束符:
    local int sum = 0
    for i = 1, 100
    do
    sum = sum + i
    end
    --  输出结果为 5050
    print(sum)
    要遍历myArray,首先需要知道tables的长度,只需要在变量前加一个#
    号即可:
    for i = 1, #myArray
    do
    print(myArray[i])
    end

    除此之外,Lua还提供了内置函数ipairs,使用for index,value
    ipairs(tables)可以遍历出所有的索引下标和值:
    for index,value in ipairs(myArray)
    do
    print(index)
    print(value)
    end
    (b)while
    下面代码同样会计算1到100的和,只不过使用的是while循环,while循
    环同样以end作为结束符。
    local int sum = 0
    local int i = 0
    while i <= 100
    do
    sum = sum +i
    i = i + 1
    end
    -- 输出结果为 5050
    print(sum)
    (c)if else
    要确定数组中是否包含了jedis,有则打印true,注意if以end结尾,if后
    紧跟then:
    local tables myArray = {"redis", "jedis", true, 88.0}
    for i = 1, #myArray
    do
    if myArray[i] == "jedis"
    then
    print("true")
    break
    else
    --do nothing
    end
    end

    (3)哈希
    如果要使用类似哈希的功能,同样可以使用tables类型,例如下面代码
    定义了一个tables,每个元素包含了key和value,其中strings1..string2是将两
    个字符串进行连接:
    local tables user_1 = {age = 28, name = "tome"}
    --user_1 age is 28
    print("user_1 age is " .. user_1["age"])
    如果要遍历user_1,可以使用Lua的内置函数pairs:
    for key,value in pairs(user_1)
    do print(key .. value)
    end
    2.函数定义
    在Lua中,函数以function开头,以end结尾,funcName是函数名,中间部
    分是函数体:
    function funcName()
    ...
    end
    contact 函数将两个字符串拼接:
    function contact(str1, str2)
    return str1 .. str2
    end
    --"hello world"
    print(contact("hello ", "world"))
    注意
    本书只是介绍了Lua部分功能,因为Lua的全部功能已经超出本书的范
    围,读者可以购买相应的书籍或者到Lua的官方网站(http://www.lua.org/)
    进行学习。

    3.4.3 Redis与Lua
    1.在Redis中使用Lua
    在Redis中执行Lua脚本有两种方法:eval和evalsha。
    (1)eval
    eval  脚本内容 key 个数 key 列表 参数列表
    下面例子使用了key列表和参数列表来为Lua脚本提供更多的灵活性:
    127.0.0.1:6379> eval 'return "hello " .. KEYS[1] .. ARGV[1]' 1 redis world
    "hello redisworld"
    此时KEYS[1]="redis",ARGV[1]="world",所以最终的返回结果
    是"hello redisworld"。
    如果Lua脚本较长,还可以使用redis-cli--eval直接执行文件。
    eval命令和--eval参数本质是一样的,客户端如果想执行Lua脚本,首先
    在客户端编写好Lua脚本代码,然后把脚本作为字符串发送给服务端,服务
    端会将执行结果返回给客户端,整个过程如图3-7所示。

    (2)evalsha
    除了使用eval,Redis还提供了evalsha命令来执行Lua脚本。如图3-8所
    示,首先要将Lua脚本加载到Redis服务端,得到该脚本的SHA1校验和,
    evalsha命令使用SHA1作为参数可以直接执行对应Lua脚本,避免每次发送
    Lua脚本的开销。这样客户端就不需要每次执行脚本内容,而脚本也会常驻
    在服务端,脚本功能得到了复用。

     

    加载脚本:script load命令可以将脚本内容加载到Redis内存中,例如下
    面将lua_get.lua加载到Redis中,得到SHA1
    为:"7413dc2440db1fea7c0a0bde841fa68eefaf149c"
    # redis-cli script load "$(cat lua_get.lua)"
    "7413dc2440db1fea7c0a0bde841fa68eefaf149c"
    执行脚本:evalsha的使用方法如下,参数使用SHA1值,执行逻辑和
    eval一致。
    evalsha  脚本 SHA1 值 key 个数 key 列表 参数列表
    所以只需要执行如下操作,就可以调用lua_get.lua脚本:
    127.0.0.1:6379> evalsha 7413dc2440db1fea7c0a0bde841fa68eefaf149c 1 redis world
    "hello redisworld"
    2.Lua的Redis API
    Lua可以使用redis.call函数实现对Redis的访问,例如下面代码是Lua使用
    redis.call调用了Redis的set和get操作:
    redis.call("set", "hello", "world")
    redis.call("get", "hello")
    放在Redis的执行效果如下:
    127.0.0.1:6379> eval 'return redis.call("get", KEYS[1])' 1 hello
    "world"
    除此之外Lua还可以使用redis.pcall函数实现对Redis的调用,redis.call和
    redis.pcall的不同在于,如果redis.call执行失败,那么脚本执行结束会直接返
    回错误,而redis.pcall会忽略错误继续执行脚本,所以在实际开发中要根据
    具体的应用场景进行函数的选择。 

    开发提示
    Lua可以使用redis.log函数将Lua脚本的日志输出到Redis的日志文件中,
    但是一定要控制日志级别。
    Redis3.2提供了Lua Script Debugger功能用来调试复杂的Lua脚本,具体
    可以参考:http://redis.io/topics/ldb。
    3.4.4 案例
    Lua脚本功能为Redis开发和运维人员带来如下三个好处:
    ·Lua脚本在Redis中是原子执行的,执行过程中间不会插入其他命令。
    ·Lua脚本可以帮助开发和运维人员创造出自己定制的命令,并可以将这
    些命令常驻在Redis内存中,实现复用的效果。
    ·Lua脚本可以将多条命令一次性打包,有效地减少网络开销。
    下面以一个例子说明Lua脚本的使用,当前列表记录着热门用户的id,
    假设这个列表有5个元素,如下所示:
    127.0.0.1:6379> lrange hot:user:list 0 -1
    1) "user:1:ratio"
    2) "user:8:ratio"
    3) "user:3:ratio"
    4) "user:99:ratio"
    5) "user:72:ratio"
    user:{id}:ratio代表用户的热度,它本身又是一个字符串类型的键:
    127.0.0.1:6379> mget user:1:ratio user:8:ratio user:3:ratio user:99:ratio
    user:72:ratio
    1) "986"
    2) "762"
    3) "556"
    4) "400"
    5) "101"
    现要求将列表内所有的键对应热度做加1操作,并且保证是原子执行,
    此功能可以利用Lua脚本来实现。
    1)将列表中所有元素取出,赋值给mylist:
    local mylist = redis.call("lrange", KEYS[1], 0, -1)
    2)定义局部变量count=0,这个count就是最后incr的总次数:
    local count = 0
    3)遍历mylist中所有元素,每次做完count自增,最后返回count:
    for index,key in ipairs(mylist)
    do
    redis.call("incr",key)
    count = count + 1
    end
    return count
    将上述脚本写入lrange_and_mincr.lua文件中,并执行如下操作,返回结
    果为5。
    redis-cli --eval lrange_and_mincr.lua hot:user:list
    (integer) 5
    执行后所有用户的热度自增1:
    127.0.0.1:6379> mget user:1:ratio user:8:ratio user:3:ratio user:99:ratio
    user:72:ratio
    1) "987"
    2) "763"
    3) "557"
    4) "401"
    5) "102"
    本节给出的只是一个简单的例子,在实际开发中,开发人员可以发挥自
    己的想象力创造出更多新的命令。

    3.4.5 Redis如何管理Lua脚本
    Redis提供了4个命令实现对Lua脚本的管理,下面分别介绍。
    (1)script load
    script load script
    此命令用于将Lua脚本加载到Redis内存中,前面已经介绍并使用过了,
    这里不再赘述。
    (2)script exists
    scripts exists sha1 [sha1  … ]
    此命令用于判断sha1是否已经加载到Redis内存中:
    127.0.0.1:6379> script exists a5260dd66ce02462c5b5231c727b3f7772c0bcc5
    1) (integer) 1
    返回结果代表sha1[sha1…]被加载到Redis内存的个数。
    (3)script flush
    script flush
    此命令用于清除Redis内存已经加载的所有Lua脚本,在执行script flush
    后,a5260dd66ce02462c5b5231c727b3f7772c0bcc5不再存在:
    127.0.0.1:6379> script exists a5260dd66ce02462c5b5231c727b3f7772c0bcc5
    1) (integer) 1
    127.0.0.1:6379> script flush
    OK
    127.0.0.1:6379> script exists a5260dd66ce02462c5b5231c727b3f7772c0bcc5
    1) (integer) 0
    (4)script kill
    script kill
    此命令用于杀掉正在执行的Lua脚本。如果Lua脚本比较耗时,甚至Lua
    脚本存在问题,那么此时Lua脚本的执行会阻塞Redis,直到脚本执行完毕或
    者外部进行干预将其结束。下面我们模拟一个Lua脚本阻塞的情况进行说
    明。
    下面的代码会使Lua进入死循环:
    while 1 == 1
    do
    end
    执行Lua脚本,当前客户端会阻塞:
    127.0.0.1:6379> eval 'while 1==1 do end' 0
    Redis提供了一个lua-time-limit参数,默认是5秒,它是Lua脚本的“超时
    时间”,但这个超时时间仅仅是当Lua脚本时间超过lua-time-limit后,向其他
    命令调用发送BUSY的信号,但是并不会停止掉服务端和客户端的脚本执
    行,所以当达到lua-time-limit值之后,其他客户端在执行正常的命令时,将
    会收到“Busy Redis is busy running a script”错误,并且提示使用script kill或者
    shutdown nosave命令来杀掉这个busy的脚本:

    127.0.0.1:6379> get hello
    (error) BUSY Redis is busy running a script. You can only call SCRIPT KILL or
    SHUTDOWN NOSAVE.
    此时Redis已经阻塞,无法处理正常的调用,这时可以选择继续等待,
    但更多时候需要快速将脚本杀掉。使用shutdown save显然不太合适,所以选
    择script kill,当script kill执行之后,客户端调用会恢复:
    127.0.0.1:6379> script kill
    OK
    127.0.0.1:6379> get hello
    "world"
    但是有一点需要注意,如果当前Lua脚本正在执行写操作,那么script
    kill将不会生效。例如,我们模拟一个不停的写操作:
    while 1==1
    do
    redis.call("set","k","v")
    end
    此时如果执行script kill,会收到如下异常信息:
    (error) UNKILLABLE Sorry the script already executed write commands against the
    dataset. You can either wait the script termination or kill the server in a
    hard way using the SHUTDOWN NOSAVE command.
    上面提示Lua脚本正在向Redis执行写命令,要么等待脚本执行结束要么
    使用shutdown save停掉Redis服务。可见Lua脚本虽然好用,但是使用不当破
    坏性也是难以想象的。

  • 相关阅读:
    【蓝桥杯单片机】五、DS18B20
    AI智能网关在工业物联网领域有哪些应用优势
    对于视频处理方法的初步研究以及第一印象
    R语言使用caret包的getModelInfo函数获取caret包中提供的模型算法列表
    生成式AI - 大模型(LLM)提示工程(Prompt)技巧
    计算机操作系统 第三章:处理机调度与死锁
    解决发邮件错误javax.mail.MessagingException: Could not connect to SMTP host
    24计算机考研调剂 | 重庆工商大学
    力扣:随即指针138. 复制带随机指针的链表
    【Java初阶】面向对象三大特性之继承
  • 原文地址:https://blog.csdn.net/tysonchiu/article/details/125609179