标题:【Redis13】Redis基础:事务与Lua脚本命令

文章目录
    分类:存储运维 标签:Redis

    Redis基础学习:事务与Lua脚本命令

    对于一个传统关系型数据库系统来说,事务是非常重要的一个组成部分。但是,在 NoSQL 相关的数据库中,为了效率以及实现形式的不同,事务远达不到真正的关系型数据库中的那种 ACID 的控制级别。今天,我们就来学习一下 Redis 中的事务操作。另外,我们还会简单地看一下在 Redis 中如何去执行 Lua 脚本程序。

    事务

    在 Redis 中,事务其实就是先使用 MULTI 开启事务操作,然后把每次执行的命令放到一个队列中,当执行 EXEC 命令后才会真正的执行队列中的所有命令。

    // 客户端1
    127.0.0.1:6379> MULTI
    OK
    127.0.0.1:6379(TX)> set a 111
    QUEUED
    127.0.0.1:6379(TX)> incr a
    QUEUED
    
    // 客户端2
    127.0.0.1:6379> get a
    (nil)
    
    // 客户端1
    127.0.0.1:6379(TX)> exec
    OK
    112
    
    // 客户端2
    127.0.0.1:6379> get a
    "112"
    

    我们在第一个客户端开启了事务,然后设置一个 a 的值为 111 ,并且 INCR 一下,也就是加 1 。此时还没有执行 EXEC 命令,然后在客户端2查询 a ,会发现 a 还是空的。接着我们在客户端1执行 EXEC 后,a 数据正式写入并且加1,所有的客户端都可以访问到了。在执行事务的时候,命令行会很明显的显示一个 TX 标识。


    既然是事务,如果不想执行了,是不是可以回滚?不不不,在 Redis 中没有回滚一说,因为命令只是在队列中,并没有真正被执行,所以自然也不能叫做回滚,而是叫做丢弃 DISCARD 。

    127.0.0.1:6379> MULTI
    OK
    127.0.0.1:6379(TX)> set a 111
    QUEUED
    127.0.0.1:6379(TX)> DISCARD
    OK
    
    127.0.0.1:6379> get a
    "112"

    上面的例子中,我们在事务中尝试重新将 a 的值设置为 111 ,但最后选择了 DISCARD ,这样后面再访问的时候 a 的值就不会发生变化。


    好了,接下来我们测试一下 Redis 事务的原子性和隔离性。先看看隔离性,也就是一个事务在执行的时候会不会受到外部的干扰。

    // 客户端1
    127.0.0.1:6379> MULTI
    OK
    127.0.0.1:6379(TX)> incr a
    QUEUED
    
    // 客户端2
    127.0.0.1:6379> set a 456
    OK
    
    // 客户端1
    127.0.0.1:6379(TX)> EXEC
    457
    127.0.0.1:6379> get a
    457

    从例子中可以看出,客户端2在客户端1的事务未提交时修改了 a 的数据,这时客户端1进行了事务的提交,结果是在客户端2修改后的数据上进行了 INCR 操作。


    可以说,完全没有隔离性,同时Redis 中也没有事务隔离级别的概念。但是这样的操作似乎会带来问题呀?没错,隔离性对于数据的准确性会有很大的影响,银行转账的例子大家在学 MySQL 的时候相信也已经听烦了。那么在 Redis 中如何解决这种情况呢?


    在 Redis 中,实现的是一种 乐观锁 机制。之前学习 MySQL 中的锁时,我们已经学习过,MySQL 中所有的锁相关操作都是 悲观锁 ,同时我们可以通过版本字段或者时间字段之类的来实现 乐观锁 。在 Redis 中,我们可以通过 WATCH 实现乐观锁。

    // 客户端1
    127.0.0.1:6379> WATCH a
    OK
    127.0.0.1:6379> MULTI
    OK
    127.0.0.1:6379(TX)> INCR a
    QUEUED
    
    // 客户端2
    127.0.0.1:6379> set a 789
    OK
    
    // 客户端1
    127.0.0.1:6379(TX)> EXEC
    
    127.0.0.1:6379> get a
    789

    看出来是什么意思了吗?乐观锁 就是信任其他人不会修改数据,如果发生了修改,自己就不更新了。WATCH 就是这个意思,在事务开始前,我们通过 WATCH 监控 a 这个 key ,如果在事务的执行过程中,有其它的客户端修改了 a 的数据,那么事务在 EXEC 时,就不会执行。


    上面的例子中可以看到,EXEC 执行后未返回任何内容,同时客户端2设置的 789 也并没有 INCR 数据。这就是乐观锁的应用。在实际的业务开发中,可以查看 EXEC 返回的结果来确定事务是否正常执行,如果没有返回信息,说明有其它客户端修改了数据。那么我们就可以再次开启事务进行操作。


    看完了隔离性,再来看看原子性,原子性说的是事务提交要么全部成功,要么全部失败回滚。在 Redis 中,如果在事务执行中间发生了异常,那么事务会出现两种情况,我们一个一个来看。


    第一种情况是运行时异常,比如使用的命令参数出现了错误,但不影响其它命令的执行,这时只有出问题的命令不会执行,其它的命令还是会执行,这种情况其实是非原子性的。

    127.0.0.1:6379> MULTI
    OK
    127.0.0.1:6379(TX)> INCR a
    QUEUED
    127.0.0.1:6379(TX)> set b 222 121
    QUEUED
    127.0.0.1:6379(TX)> EXEC
    1) (integer) 790
    2) (error) ERR syntax error

    另一种情况是编译型异常,比如使用了错误的命令,这时整个事务的提交都会被丢弃,这个才是原子性的。

    127.0.0.1:6379> MULTI
    OK
    127.0.0.1:6379(TX)> INCR a
    QUEUED
    127.0.0.1:6379(TX)> setget b 123 112
    (error) ERR unknown command `setget`, with args beginning with: `b`, `123`, `112`,
    127.0.0.1:6379(TX)> EXEC
    (error) EXECABORT Transaction discarded because of previous errors.
    127.0.0.1:6379> get a
    "790"

    综上所述,Redis 中的事务就像我们最开始讲的那样,其实就是一个命令执行队列,并不是完全意义上的关系型数据库中的事务的概念。在面试的时候要注意面试官在这里挖坑哦!

    Lua脚本

    Lua脚本是非常轻量级的脚本语言,同时也是受到 Nginx 和 Redis 所支持的一种脚本语言。怎么说呢,就是 Redis 可以直接运行或通过 Lua 脚本进行一些操作。


    不过我对 Lua 并不熟悉,所以这里也就是演示一下在 Redis 中去执行或加载操作 Lua 脚本的一些命令。


    首先看一下如何执行一段 Lua 脚本。

    127.0.0.1:6379> EVAL "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 a b 111 222
    1) "a"
    2) "b"
    3) "111"
    4) "222"

    EVAL 命令后面可以跟着一段 Lua 脚本,后面的参数中,2 表示有两个 KEYS ,a 和 b 就是在 Lua 中可以获取到的 KEYS 数组中的内容。后面的 111 和 222 其实就是对应到 Lua 脚本中 ARGV 数组中的数据。


    来个更实际一点的例子,比如我们要为 a 这个 key 赋值。

    127.0.0.1:6379> EVAL "redis.call('set', KEYS[1], ARGV[1]);return 'ok'" 1 a 111
    "ok"
    127.0.0.1:6379> get a
    "111"
    127.0.0.1:6379> EVAL "return redis.call('get', 'a')" 0
    "111"

    这个例子就明显很多了吧,我们使用的是 redis.call 这个函数,然后使用 set 命令,通过外部传值把数据传给 Lua 脚本中的 KEYS 和 ARGV ,这样就执行了一个 set a 111 的操作。同样的,我们也可以通过 redis.call 来执行其它的 Redis 命令。


    除了 EVAL 命令之外,还有一套 SCRIPT 命令,也是用来操作 Lua 脚本的。SCRIPT 是一套复合命令,它的子命令包括下面这些。

    127.0.0.1:6379> SCRIPT HELP
     1) SCRIPT <subcommand> [<arg> [value] [opt] ...]. Subcommands are:
     2) DEBUG (YES|SYNC|NO)
     3)     Set the debug mode for subsequent scripts executed.
     4) EXISTS <sha1> [<sha1> ...]
     5)     Return information about the existence of the scripts in the script cache.
     6) FLUSH [ASYNC|SYNC]
     7)     Flush the Lua scripts cache. Very dangerous on replicas.
     8)     When called without the optional mode argument, the behavior is determined by the
     9)     lazyfree-lazy-user-flush configuration directive. Valid modes are:
    10)     * ASYNC: Asynchronously flush the scripts cache.
    11)     * SYNC: Synchronously flush the scripts cache.
    12) KILL
    13)     Kill the currently executing Lua script.
    14) LOAD <script>
    15)     Load a script into the scripts cache without executing it.
    16) HELP
    17)     Prints this help.

    那么 SCRTIP 命令和 EVAL 命令有什么区别呢?EVAL 是直接执行,而 SCRIPT 可以通过 SCRIPT LOAD 命令将脚本先加载到 Redis 中,但并不马上执行,之后我们可以通过 EVALSHA 命令来执行,就像下面这样。

    127.0.0.1:6379> SCRIPT LOAD "return redis.call('get', 'a')"
    "b2dc80c45e350e7bf2b3fc26fb0451ee65259785"
    127.0.0.1:6379> EVALSHA b2dc80c45e350e7bf2b3fc26fb0451ee65259785 0
    "111"

    看出来什么意思了吧,SCRIPT LOAD 返回一个哈希签名,然后 EVALSHA 可以直接使用这个签名去运行之前加载进来的脚本。


    SCRIPT EXISTS 用于判断给定的签名是否已经加载在当前的服务器环境中。

    127.0.0.1:6379> SCRIPT EXISTS b2dc80c45e350e7bf2b3fc26fb0451ee65259785
    1) (integer) 1
    127.0.0.1:6379> SCRIPT EXISTS b2dc80c45e350e7bf2b3fc26fb0451ee65259784
    1) (integer) 0

    SCRIPT FLUSH 则是清除所有已加载的脚本。

    127.0.0.1:6379> SCRIPT FLUSH
    OK
    127.0.0.1:6379> SCRIPT EXISTS b2dc80c45e350e7bf2b3fc26fb0451ee65259785
    1) (integer) 0

    最后,我们再来看一下如何去运行一个外部的 Lua 脚本文件。

    ➜  ~ vim get.lua
    return redis.call('get', 'a')
    
    
    ➜  ~ redis-cli --eval get.lua
    "111"

    通过 redis-cli --eval 就可以在外部去加载运行一个指定 .lua 文件,这样其实我们就可以写一些自己的 Lua 脚本,比如说预热数据之类的,通过外部执行的方式就能够利用语言优势来批量、循环的操作数据。


    Lua 脚本在 Redis 中有非常重要的作用,虽说我们可能平时用不到,但是,在很多框架中,比如 Laravel 或者 Java 的 Redisson 中,都大量频繁地使用了 Lua 脚本。这是为啥呢?那是因为一段 Lua 脚本的执行,在 Redis 中是可以保证完全的原子性的,也就是真正的要么全部成功,要么全部失败,而不是 Redis 事务中的监视事务这种乐观锁机制。


    之前我们在学习 Laravel 框架的时候,其实就见过 Redis 配合 Lua 脚本在 Laravel 中的应用,不记得的小伙伴可以去看看视频 【Laravel系列7.7】队列系统https://mp.weixin.qq.com/s/55-wp3YIQpLSrktIlZKMow

    总结

    今天的重点很明显就是事务相关命令的学习,如果你会 Lua 的话,当然也可以在 Redis 中大展身手了。对于日常的工作来说,如果只是将 Redis 作为缓存或者实现一些简单的队列应用的话,事务也都是可有可无的,毕竟我们也不完全依赖于 Redis 来实现真正的需要强事务的功能操作。但是,这一块却又是很多面试官喜欢问的东西,所以了解一下总没坏处。

    视频链接

    微信文章地址:https://mp.weixin.qq.com/s/jsbLktJxGZOwrZeX64-mJQ

    B站视频地址:https://www.bilibili.com/video/BV1e14y1m7Ka

    微信视频地址:https://mp.weixin.qq.com/s/sOtQd5xe2GJcYBLiRcQ0Dg

    搜索
    关注