标题:【Redis13】Redis基础:事务与Lua脚本命令
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