Redis事务的定义
Redis的事务可以一次执行多个命令,并保证2个特性:
- 事务中所有的命令都会被序列化并按顺序执行,执行期间不会被其他命令打断
- 事务是原子操作,要么执行全部命令,要么不执行
简而言之:一个队列中,一次性、顺序性、排他性的执行一系列Redis命令。
工作原理
Redis事务相关命令
Redis使用MULTI, EXEC, DISCARD ,UNWATCH和 WATCH 命令来实现事务功能。
- MULTI: 用于标记一个事务块的开始。
- EXEC: 用于执行事务队列内的所有命令。
- DISCARD: 取消事务,放弃执行事务队列内的所有命令。
- WATCH: 命令用于标记要监视的key(可以多个),一旦其中有一个键被修改(或删除),之后的事务就不会执行。
- UNWATCH: 取消WATCH对所有key的监视。
Redis事务使用
事务正常提交
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> exec
1) OK
2) OK
事务取消提交
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> discard
OK
事务提交命令存在语法错误(编译器错误)
开启事务后,出现sets语法错误,导致事务提交失败,k1、k2值写入失败。
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> sets k2 v2
(error) ERR unknown command 'sets'
127.0.0.1:6379> exec
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get k1
(nil)
事务提交出现运行时错误
开启事务后,set k2命令入参数量不正确,事务提交并没有失败,事务执行时,跳过错误命令继续执行。
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> set k2 v2 v2
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> exec
1) OK
2) (error) ERR syntax error
3) OK
127.0.0.1:6379> get k1
"v1"
127.0.0.1:6379> get k2
(nil)
127.0.0.1:6379> get k3
"v3"
watch监视数据
watch监视数据的数据发生变化后,事务回滚,队列中命令不生效。
127.0.0.1:6379> watch k1
OK
127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k1 v11
QUEUED
127.0.0.1:6379> set k2 v22
QUEUED
127.0.0.1:6379> exec
(nil)
127.0.0.1:6379> get k1
"v1"
127.0.0.1:6379> get k2
(nil)
unwatch取消监视,逻辑类似从multi开始处理,事务不受影响。
127.0.0.1:6379> watch k1
OK
127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> unwatch
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k1 v11
QUEUED
127.0.0.1:6379> set k2 v22
QUEUED
127.0.0.1:6379> exec
1) OK
2) OK
127.0.0.1:6379> get k1
"v11"
127.0.0.1:6379> get k2
"v22"
细说watch命令的使用场景及原理
使用场景:
假设我们在Redis有一个标志位,根据查得的不同标志位,进行不同的逻辑处理。如果标志位发生改变,程序应该感知到变化,并且做重试或者失败处理。(例如分布式锁、限流器等)。
实现原理:
事务深入理解
为什么Redis不支持回滚?
Redis事务特殊的地方在于事务中的命令允许失败,但是Redis会继续执行其它的命令而不是回滚所有命令。这么做的原因有两点:
-
Redis 命令只在两种情况失败
- 语法错误的时候才失败(在命令输入的时候不检查语法)。
- 要执行的key数据类型不匹配:这种错误实际上是编程错误,这应该在开发阶段被测试出来,而不是生产上。
-
不考虑回滚,实现简单并高效。
Redis事务VS数据库事务
原子性
Redis的事务不保证原子性,也就是不保证所有指令同时成功或同时失败,只有决定是否开始执行全部指令的能力,没有执行到一半进行回滚的能力。
一致性
redis事务可以保证命令失败的情况下得以回滚,数据能恢复到没有执行之前的样子。但是命令运行时异常的情况,会被丢弃,不会回滚。
隔离性
Redis会保证一个事务内的命令依次执行,而不会被其它命令插入。
持久性
redis事务是不保证持久性的,这是因为redis持久化策略中不管是RDB还是AOF都是异步执行的,不保证持久性是出于对性能的考虑。
Lua脚本
前面讲到watch例子进行扩充
- 第一步,redis中查询k1 得到v1
- 第二步,代码操作v1
- 第三步,redis赋值 k1 newV1
看上去很简单的逻辑,但是存在严重的并发问题,第三步赋值的时候,如果v1的值被其他线程修改了,第三步赋值结果 就不一定正确了!!!
由此Redis引入了Lua脚本支持:
向服务器发送 lua 脚本来执行连续逻辑操作,获取脚本的响应数据。并且Redis服务器会单线程原子性执行 lua 脚本,保证 lua 脚本在处理的过程中不会被其它请求打断。
Lua脚本的基本语法可参考:菜鸟教程
Redis执行Lua脚本
命令不多,就下面这几个:
- EVAL
- EVALSHA
- SCRIPT LOAD - SCRIPT EXISTS
- SCRIPT FLUSH
- SCRIPT KILL
执行LUA脚本
- 参数1: EVAL 执行命令
- 参数2: "xxx" lua脚本字符串
- 参数3: redis key的个数 key后面是value个数
127.0.0.1:6379> eval "return 'hello world!'" 0
"hello world!"
127.0.0.1:6379> eval "return {KEYS[1],ARGV[1]}" 1 key value
1) "key"
2) "value"
127.0.0.1:6379> eval "redis.call('SET',KEYS[1], ARGV[1]); redis.call('EXPIRE',KEYS[1],ARGV[2]); return 1" 1 k1 10 60
(integer) 1
Lua脚本缓存
SCRIPT LOAD
将脚本 script 添加到Redis服务器的脚本缓存中,并返回给定脚本的 SHA1 校验和。
EVALSHA
使用脚本的 SHA1 校验和来调用这个脚本。
SCRIPT EXISTS
使用脚本的 SHA1 校验和来判断脚本是否存在。
SCRIPT FLUSH
清除Redis服务端所有 Lua 脚本缓存。
SCRIPT KILLS
杀死当前正在运行的 Lua 脚本,当且仅当这个脚本没有执行过任何写操作时,这个命令才生效。
127.0.0.1:6379> SCRIPT LOAD "redis.call('SET', KEYS[1], ARGV[1]);redis.call('EXPIRE', KEYS[1], ARGV[2]); return 1;"
"6aeea4b3e96171ef835a78178fceadf1a5dbe345"
127.0.0.1:6379> EVALSHA 6aeea4b3e96171ef835a78178fceadf1a5dbe345 1 k2 10 60
(integer) 1
127.0.0.1:6379> SCRIPT EXISTS 6aeea4b3e96171ef835a78178fceadf1a5dbe345
1) (integer) 1
127.0.0.1:6379> SCRIPT FLUSH
OK
127.0.0.1:6379> SCRIPT EXISTS 6aeea4b3e96171ef835a78178fceadf1a5dbe345
1) (integer) 0
总结:
- Lua脚本主要用于实现Redis命令无法实现的场景(例如多命令原子操作)
- Redis使用Lua脚本的几个优点:减少网络开销、原子性、复用
最后
本文是Redis系列的第四篇,这个系列会系统全面的梳理Redis的知识体系,如果有遗漏或者错误,欢迎留言沟通交流。
如果你都看到这了,在文章左侧有个点赞按钮,点赞支持弥金吧。