3.4、事务与Lua

事务与Lua

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

  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:bfans 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的处理机制也不尽相同。

    • 命令错误

      例如下面操作错将set命令写成了sett,属于语法错误,会造成整个事务无
      法执行,key和counter的值未发生变化:

      127.0.0.1:6379> mget key counter
      1) "hello"
      2) "100"
      127.0.0.1:6379> multi
      OK
      127.0.0.1:6379> sett key world
      (error) ERR unknown command 'sett'
      127.0.0.1:6379> incr counter
      QUEUED
      127.0.0.1:6379> exec
      (error) EXECABORT Transaction discarded because of previous errors.
      127.0.0.1:6379> mget key counter
      1) "hello"
      2) "100"
      
    • 运行时错误

      例如用户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 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命令来解
      决这类问题,下表展示了两个客户端执行命令的时序。

      时间点 客户端-1 客户端-2
      T1 set key "java"
      T2 watch key
      T3 multi
      T4 append key python
      T5 append key jedis
      T6 exec
      T7 get key

      可以看到“客户端-1”在执行multi之前执行了“watch”命令,“客户端-2”在
      “客户端-1”执行exec之前修改了key值,造成事务没有执行(exec结果为
      null),整个代码如下所示:

      #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”的特性。

  2. Lua用法简述

    Lua语言是1993年由巴西一个大学研究小组发明,其设计目标是作为嵌入式程序
    移植到其他应用程序,它是由C语言实现的,虽然简单小巧但是功能强大,所以许
    多应用都选用它作为脚本语言,尤其是在游戏领域,例如大名鼎鼎的暴雪公司将
    Lua语言引入到“魔兽世界”这款游戏中,Rovio公司将Lua语言作为“愤怒的小鸟”
    这款火爆游戏的关卡升级引擎,Web服务器Nginx将Lua语言作为扩展,增强自身
    功能。Redis将Lua作为脚本语言可以帮助开发者定制自己的Redis命令,在这之
    前,必须修改源码。以下是对Lua语言的使用做一个基本的介绍。

    • 数据类型及其逻辑处理

      Lua语言提供了如下几种数据类型:booleans(布尔)、numbers(数值)
      、strings(字符串)、tables(表格),和许多高级语言相比,相对简
      单。下面将结合例子对Lua的基本数据类型和逻辑处理进行说明。

      (1) 字符串

      下面定义一个字符串类型的数据:

      local strings val = "world"
      

      其中,local代表val是一个局部变量,如果没有local代表室全局变量。
      print函数可以打印出变量的值,例如下面代码将打印world,其中“--”是
      Lua语言的注释。

      -- 结果是“world”
      print(hello)
      

      (2) 数组

      在Lua中,如果要使用类似数组的功能,可以用tabels类型,下面代码使用
      定义了一个tables类型的变量myArray,但和大多数编程语言不同的是,
      Lua的数据下标从1开始计算:

      local tables myArray = {"redis", "jedis", true, 88.0}
      --true
      print(myArray[3])
      

      如果想遍历这个数组,可以使用for和while,这些关键字和许多编程语言是
      一致的。

      • 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, #myArrary
        do
            print(myArray[i])
        end
        

        除此之外,Lua还提供了内置函数ipairs,使用for index, value
        ipairs(tables)可以遍历出所有的索引下标和值:

        for index, value in ipairs(myArray)
        do
            print(index)
            print(index)
        end
        
      • 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)
        
      • 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 .. strings2是将两个字符串进行连接:

      local tabels 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 paris(user_1)
      do print(key .. value)
      end
      
    • 函数定义

      在Lua中,函数以funciton开头,以end结尾,funcName是函数名,中间部
      分是函数体:

      function funcName()
          ...
      end
      

      contact函数将两个字符串拼接:

      function contact(str1, str2)
          return str1 .. str2
      end
      --"hello world"
      print(contact("hello","world"))
      
  3. Redis与Lua

    • 在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脚本代码,然后把脚本作为字符串发送给服务端,服务端
      会将执行结果返回给客户端,整个过程如下图:

      2019-04-07-14-11-21.png

      (2) evalsha

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

      2019-04-07-14-21-45.png

      加载脚本: script load命令可以将脚本内容加载到Redis内存中,
      例如下面将lua_get.lua加载到Redis中,得到SH1为:
      "741dc2440db1fea7c0a0bde841fa68eefaf149c"

      # redis-cli script load "$(cat lua_get.lua)"
      "741dc2440db1fea7c0a0bde841fa68eefaf149c"
      

      执行脚本: evalsha的使用方法如下,参数使用SHA1值,执行逻辑和
      eval一致。

      evalsha 脚本 SHA1 值 key 个数 key 列表 参数列表

      所以只需要执行如下操作,就可以调用lua_get.lua脚本:

      127.0.0.1:6379> evalsha 741dc2440db1fea7c0a0bde841fa68eefaf149c 1 redis world
      "hello redisworld"
      
    • 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的日
      志文件中,但是一定要控制日志级别。

  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"
    
  • Redis如何管理Lua脚本

    Redis提供了4个命令实现对Lua脚本的管理,下面分别介绍:

    • script load

      script load script

      此命令用于将Lua脚本加载到Redis内存中,前面已经介绍并使用过了,这里
      不再赘述。

    • script exists

      scripts exists sha1 [sha1 ...]

      此命令用于判断sha1是否已经加载到Redis内存中:

      127.0.0.1:6379> script exists a5260dd66ce02462c5b5231c727b3f7773c0bcc5
      (integer) 1
      

      返回结果代表sha1 [sha1 ...]被加载到Redis内存的个数。

    • script flush

      script flush

      此命令用于清除Redis内存已经加载的所有Lua脚本,在执行script flush
      后,a5260dd66ce02462c5b5231c727b3f7773c0bcc5不再存在:

      127.0.0.1:6379> script exists a5260dd66ce02462c5b5231c727b3f7773c0bcc5
      1) (integer) 1
      127.0.0.1:6379> flush
      OK
      127.0.0.1:6379> script exists a5260dd66ce02462c5b5231c727b3f7773c0bcc5
      1) (integer) 0
      
    • 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 si 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 SCTIPT 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脚本已经执行过写操作,要么等待脚本执行结束要么使用
      shutdown save停掉Redis服务。可见Lua脚本虽然好用,但是使用不当破
      坏性也是难以想象的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,826评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,968评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,234评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,562评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,611评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,482评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,271评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,166评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,608评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,814评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,926评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,644评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,249评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,866评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,991评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,063评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,871评论 2 354

推荐阅读更多精彩内容