前言
和众多其它数据库一样,Redis 作为 NoSQL 数据库也同样提供了事务机制。在 Redis 中,MULTI/EXEC/DISCARD/WATCH 这四个命令是我们实现事务的基石。
Redis 中事务的实现特征
- 在事务中的所有命令都将会被串行化的顺序执行
- 和关系型数据库中的事务相比,在 Redis 事务中如果有某一条命令执行失败,其后的命令仍然会
被继续执行。 - 我们可以通过MULTI命令开启一个事务,有关系型数据库开发经验的人可以将其理解为”BEGIN TRANSACTION”语句。在该语句之后执行的命令都将被视为事务之内的操作,最后我们可以通过执行EXEC/DISCARD 命令来提交/回滚该事务内的所有操作。这两个 Redis 命令可被视为等同于关系型数据库中的 COMMIT/ROLLBACK语句。
- 在事务开启之前,出现通讯故障并导致网络断开,其后所有待执行的语句都将不会被服务器执行。然而如果网络中断事件是发生在客户端执行 EXEC 命令之后,那么该事务中的所有命令都会被服务器执行。
- 当使用 Append-Only 模式时,Redis 会通过调用系统函数write 将该事务内的所有写操作在本次调用中全部写入磁盘。 然而如果在写入的过程中出现系统崩溃,如电源故障导致的宕机,那么此时也只有部分数据被写入到磁盘,而另外一部分数据却已经丢失。Redis 服务器会在重新启动时执行一系列必要的一致性检测,一旦发现类似问题,就会立即退出并给出相应的错误提示。此时,我们就要充分利用 Redis工具包中提供的redis-check-aof 工具,该工具可以帮助我们定位到数据不一致的错误,并将已经写入的部分数据进行回滚。修复之后我们就可以再次重新启动 Redis 服务器了。
相关命令列表
MULTI
用于标记事务的开始,其后执行的命令都将被存入命令队列,直
到执行 EXEC 时,这些命令才会被原子的执行。EXEC
执行在一个事务内命令队列中的所有命令,同时将当前连接的状
态恢复为正常状态,即非事务状态。如果在事务中执行了
WATCH 命令,那么只有当WATCH 所监控的 Keys 没有被修
改的前提下,EXEC 命令才能执行事务队列中的所有命令,否则
EXEC 将放弃当前事务中的所有命令。DISCARD
回滚事务队列中的所有命令,同时再将当前连接的状态恢复为正
常状态,即非事务状态。如果 WATCH 命令被使用,该命令将
UNWATCH 所有的 Keys。WATCH
在 MULTI 命令执行之前,可以指定待监控的 Keys,然而在执
行 EXEC 之前,如果被监控的 Keys 发生修改,EXEC 将放弃执
行该事务队列中的所有命令。UNWATCH
取消当前事务中指定监控的 Keys,如果执行了 EXEC 或
DISCARD 命令,则无需再手工执行该命令了,因为在此之后,
事务中所有被监控的 Keys 都将自动取消。
Redis的事务是原子性的吗?
原子性:
数据库中的某个事务A中要更新t1表、t2表的某条记录,当事务提交,t1、t2两个表都被更新,若其中一个表操作失败,事务将回滚。
非原子性:
数据库中的某个事务A中要更新t1表、t2表的某条记录,当事务提交,t1、t2两个表都被更新,若其中一个表操作失败,另一个表操作继续,事务不会回滚。(当然对于关系型数据库不会出现非原子性)
至于Redis的事务是否为原子性,我们通过一些例子进行观察。
- 事务正常执行
给k1、k2分别赋值,在事务中修改k1、k2,执行事务后,查看k1、k2值都被修改。
127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> set k2 v2
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set k1 11
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
"11"
127.0.0.1:6379> get k2
"22"
127.0.0.1:6379>
- 事务失败处理(语法错误)
语法错误(编译器错误),在开启事务后,修改k1值为11,k2值为22,但k2语法错误,最终导致事务提交失败,k1、k2保留原值。
127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> set k2 v2
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set k1 11
QUEUED
127.0.0.1:6379> sets k2 22
(error) ERR unknown command `sets`, with args beginning with: `k2`, `22`,
127.0.0.1:6379> exec
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get k1
"v1"
127.0.0.1:6379> get k2
"v2"
127.0.0.1:6379>
看到这里,可以确定Redis的事务是原子性的吗?
- 事务失败处理(运行时错误)
Redis类型错误(运行时错误),在开启事务后,修改k1值为11,k2值为22,但将k2的类型作为List,在运行时检测类型错误,最终导致事务提交失败,此时事务并没有回滚,而是跳过错误命令继续执行, 结果k1值改变、k2保留原值。
127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> set k1 v2
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set k1 11
QUEUED
127.0.0.1:6379> lpush k2 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 k1
"11"
127.0.0.1:6379> get k2
"v2"
127.0.0.1:6379>
为啥语法错误就回滚,类型错误就不回滚?
因为前者是在入队之前就监测到的,后者是在执行过程中报错,redis为了性能而采取简单的事务。
严格来说Redis的命令是原子性的,事务并不是原子性的,我们要让Redis事务完全具有事务回滚的能力,需要借助于命令WATCH来实现。
WATCH
WATCH提供CAS功能,使用WATCH来监控某些键值对,然后使用MULTI命令来开启事务,执行对数据结构操作的各种命令,此时这些命令入队列。
当使用EXEC执行事务时,首先会比对WATCH所监控的键值对,如果没发生改变,它会执行事务队列中的命令,提交事务;如果发生变化,将不会执行事务中的任何命令,同时事务回滚。当然无论是否回滚,Redis都会取消执行事务前的WATCH命令。
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"
127.0.0.1:6379>
DISCARD
不是用EXEC结束事务,而是用DISCARD取消事务,则命令都不会执行。
redis 127.0.0.1:6379> set t2 tt
OK
redis 127.0.0.1:6379> multi
OK
redis 127.0.0.1:6379> set t2 ttnew
QUEUED
redis 127.0.0.1:6379> discard
OK
redis 127.0.0.1:6379> get t2
"tt"