「进击Redis」十二、彻底搞懂 Redis 事务

2,371

前言

接上篇Redis Pipeline 这一篇就够了 后我又来了。讲道理,Pipeline这一篇弄得还算挺全的,就是缺了一个具体客户端实现的代码(这个会在后面加进去的,一点都不带慌的)。还有就是我的排版真的是越来越好了,风格也没有那么单一了。看在我这么用心的份上好哥哥们还不点关注、点赞吗(疯狂暗示)。这篇是 Redis 系列的第十二篇了,不容易呀,不过好哥哥们的点赞加关注是我持续的动力(再次暗示,别愣着呀)。
这篇篇幅会有点长,好哥哥们好好看(夸奖),我可不能做个标题党,让我们继续肉弹冲击,冲冲冲.....
暗示

概述

熟悉关系型数据库的好哥哥们对事务都比较了解吧。简单地说就是事务表示一组动作,要么全部执行要么全部不执行,像Mysql中的事务还是挺复杂的,要保证事务四大特征(ACID)原子性(A)、一致性 ©、隔离性(I)、持久性(D),又会引入像事务的隔离级别等等东西。
然而 Redis 只提供了简单的事务功能。本质是一组命令的集合。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。命令的执行过程是是原子顺序执行的,但是不能保证原子性

命令解析

Redis 提供了MULTIEXECDISCARDWATCHUNWATCH命令。

 ## 开启事务,redis会将后续的命令逐个放入队列中,然后使用EXEC命令来原子化执行这个命令系列。
 MULTI
 ## 提交命令,执行事务中的所有操作命令。
 EXEC
 ## 取消事务,放弃执行事务块中的所有命令。  
 DISCARD
 ## 监视一个或多个key,如果事务在执行前,这个key(或多个key)被其他命令修改,则事务被中断,不会执行事务 中的任何命令。
 WATCH
 ## 取消WATCH对所有key的监视
 UNWATCH

怎么玩

Redis 事务会根据不同的使用场景或者执行命令时出现错误的不同,处理机制也不尽相同。主要分成了以下的集中场景:

正常执行

在开启一个事务后,在EXEC 未提交前,都是将命令入队列(QUEUED)。只有执行 EXEC 命令时 Redis 才会逐条执行命令并返回结果。

127.0.0.1:6379> set test v1
OK
127.0.0.1:6379> set test1 v2
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set test 11
QUEUED
127.0.0.1:6379> set test1 22
QUEUED
127.0.0.1:6379> EXEC
1) OK
2) OK
127.0.0.1:6379> get k1
"11"
127.0.0.1:6379> get k2
"22"

语法错误(编译器错误)

当 Redis 开启一个事务后,添加对应的命令出现了语法错误时,会导致事务提交失败。这种情况下事务中队列的命令都不会被执行(因为没有提交事务,Redis 并没有执行命令),所以下面的代码testtest2 还是会保留事务提交前的值。

127.0.0.1:6379> set test v1
OK
127.0.0.1:6379> set test2 v2
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set test 11
QUEUED
127.0.0.1:6379> sets test2 22
(error) ERR unknown command `sets`, with args beginning with: `test2`, `22`,
127.0.0.1:6379> exec
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get test
"v1"
127.0.0.1:6379> get test2
"v2"

Redis 类型错误(运行时错误)

当 Redis 开启一个事务后,添加对应的命令到 Redis 并没有报编译错误。也就是说事务里面的命令 Redis 都可以执行,在运行时检测到了类型错误,最终导致事务提交失败,此时事务并没有回滚,而是跳过错误命令继续执行。所以事务提交后未报错的命令值已经被修改,报错的值未被替换。

127.0.0.1:6379> set test v1
OK
127.0.0.1:6379> set test2 v2
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set test 11
QUEUED
127.0.0.1:6379> lpush test2 22
QUEUED
127.0.0.1:6379> EXEC
1) OK
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
127.0.0.1:6379> get test
"11"
127.0.0.1:6379> get test2
"v2"
为什么 Redis 不支持回滚:

多数事务失败是由语法错误或者数据结构类型错误导致的,语法错误说明在命令入队前就进行检测的,而类型错误是在执行时检测的,Redis 为提升性能而采用这种简单的事务,这是不同于关系型数据库的,特别要注意区分。

DISCARD 取消事务

取消事务和Mysql中的ROLLBACK 不同,ROLLBACK 会结束用户的事务,并撤销正在进行的所有未提交的修改。在 Redis 中,DISCARD取消事务很好理解,因为开启事务后所有的命令都在对列里面,DISCARD就是不提交执行,删除对应队列的命令。

127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set test 33
QUEUED
127.0.0.1:6379> set test2 34
QUEUED
127.0.0.1:6379> DISCARD
OK

WATCH 监视 key

严格意义上讲Redis 的命令是原子性的,而事务是非原子性的,那怎么样才能让 Redis 事务完全具有事务回滚的能力呢?答案是需要借助于WATCH命令来实现。
Redis 使用WATCH命令来决定事务是继续执行还是回滚,确保事务中的key没有被其他客户端修改过,才执行事务,否则不执行(类似乐观锁)。需要在MULTI之前使用WATCH来监控某些键值对,然后使用MULTI命令来开启事务,执行对数据结构操作的各种命令,此时这些命令入队列。
当使用EXEC执行事务时,首先会比对WATCH所监控的键值对,如果没发生改变,它会执行事务队列中的命令,提交事务;如果发生变化,将不会执行事务中的任何命令,同时事务回滚。当然无论是否回滚,Redis 都会取消执行事务前的WATCH命令。
需要注意的是,使用WATCH也不能回滚 Redis 类型错误(运行时错误)的执行,队列中的命令正常执行是不会回滚的。

127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> set k2 v2
OK
127.0.0.1:6379> WATCH k1
OK
127.0.0.1:6379> set k1 11
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set k1 12
QUEUED
127.0.0.1:6379> set k2 22
QUEUED
127.0.0.1:6379> EXEC
(nil)
127.0.0.1:6379> get k1
"11"
127.0.0.1:6379> get k2
"v2"

UNWATCH 取消监视所有 key

Redis 提供了UNWATCH来取消 WATCH 命令对所有 key 的监视。

127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> set k2 v2
OK
127.0.0.1:6379> WATCH k1
OK
127.0.0.1:6379> set k1 11
OK
127.0.0.1:6379> UNWATCH
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set k1 12
QUEUED
127.0.0.1:6379> set k2 22
QUEUED
127.0.0.1:6379> exec
1) OK
2) OK
127.0.0.1:6379> get k1
"12"
127.0.0.1:6379> get k2
"22"

总结

  1. Redis不支持事务中的回滚特性,同时无法实现命令之间的逻辑关系计算,当事务失败时,Redis 也不会进行任何的重试或者回滚动作。当然这也体现了Rediskeep it simple的特性。
  2. Redis命令的执行过程是是原子顺序执行的,但是不能保证原子性
  3. Redis事务没有隔离级别的概念。
  4. WATCH命令类似于乐观锁,简单点说就是监控Redis对应的key,如果自己的值和Redis中的值对不上事务队列将不会被执行(是不是有点像JAVACAS机制)。
  5. Redis中的事务不适合做逻辑判断,这一点和隔离级别有点关系。有的逻辑需要先存起来,然后在拿出来做其他业务逻辑,而 Redis 只是把命令放到队列中。
  6. 当客户端处于非事务状态下时,所有发送给服务器端的命令都会立即被Redis服务器执行(当然还是按照排队来执行)。
  7. Redis 在执行命令是单线程的,这能保证在执行事务时,不会对事务进行中断,也不会执行其他客户端的命令,事务可以运行直到执行完所有事务队列中的命令为止。因此,Redis 的事务是总是带有隔离性的。
  8. Redis 事务的持久性是基于它本身的持久性机制(AOFRDB)。

Redis 事务一致性

眼尖的好哥哥们是不是发现了在总结环节没有发现关于 Redis 事务一致性 的解析,这个有必要单独拿出来讲一下的是,猛男我在网上和一些书看到了这个观点是 Redis 的事务是支持一致性的。那猛男我就有一些疑问了,以下纯属个人观点(喷可以,别动手)。
首先我的结论是 Redis 的事务是不支持一致性的。首先上面也提到了就是 Redis 开启一个事务时,并没有发生编译错误,有可能只是key的数据结构对不上,这就会导致一个事务中有一部命令执行了,有一部分命令没有被执行。
其次,Redis 中的持久化机制导致的(这个后面会详解),Redis 提供的AOFRDB两种持久化机制。RDB 适合做冷备,保存的是某一个时间的数据快照。而AOF 适合热备,一般 AOF 会每隔 1 秒,通过一个后台线程执行一次 fsync 操作,最多丢失 1 秒钟的数据。那么问题就来了,当开启事务并添加了 6 条对应的写操作,然后提交事务,Redis 在内存中操作完成,但是还没有执行 fsync操作,但是服务器因为神秘力量发生异常或宕机,这时并没有把这 6 条对应的写操作完全写入AOF日志文件里面(可能全部丢了也可能丢了一部分),这时也造成了数据的不一致性。
所以猛男我认为Redis 事务是不具备一致性的,有不同意见的好哥哥们评论区见咯,吃我一记肉弹冲击。

本期就到这啦,有不对的地方欢迎好哥哥们评论区留言,另外求关注、求点赞

上一篇:Redis Pipeline这一篇就够了