消息中间件——Redis—事务(七)

前言

和众多其它数据库一样,Redis 作为 NoSQL 数据库也同样提供了事务机制。在 Redis 中,MULTI/EXEC/DISCARD/WATCH 这四个命令是我们实现事务的基石。

Redis 中事务的实现特征

  1. 在事务中的所有命令都将会被串行化的顺序执行
  2. 和关系型数据库中的事务相比,在 Redis 事务中如果有某一条命令执行失败,其后的命令仍然会
    被继续执行。
  3. 我们可以通过MULTI命令开启一个事务,有关系型数据库开发经验的人可以将其理解为”BEGIN TRANSACTION”语句。在该语句之后执行的命令都将被视为事务之内的操作,最后我们可以通过执行EXEC/DISCARD 命令来提交/回滚该事务内的所有操作。这两个 Redis 命令可被视为等同于关系型数据库中的 COMMIT/ROLLBACK语句。
  4. 在事务开启之前,出现通讯故障并导致网络断开,其后所有待执行的语句都将不会被服务器执行。然而如果网络中断事件是发生在客户端执行 EXEC 命令之后,那么该事务中的所有命令都会被服务器执行。
  5. 当使用 Append-Only 模式时,Redis 会通过调用系统函数write 将该事务内的所有写操作在本次调用中全部写入磁盘。 然而如果在写入的过程中出现系统崩溃,如电源故障导致的宕机,那么此时也只有部分数据被写入到磁盘,而另外一部分数据却已经丢失。Redis 服务器会在重新启动时执行一系列必要的一致性检测,一旦发现类似问题,就会立即退出并给出相应的错误提示。此时,我们就要充分利用 Redis工具包中提供的redis-check-aof 工具,该工具可以帮助我们定位到数据不一致的错误,并将已经写入的部分数据进行回滚。修复之后我们就可以再次重新启动 Redis 服务器了。

相关命令列表

  1. MULTI

    用于标记事务的开始,其后执行的命令都将被存入命令队列,直
    到执行 EXEC 时,这些命令才会被原子的执行。

  2. EXEC

    执行在一个事务内命令队列中的所有命令,同时将当前连接的状
    态恢复为正常状态,即非事务状态。如果在事务中执行了
    WATCH 命令,那么只有当WATCH 所监控的 Keys 没有被修
    改的前提下,EXEC 命令才能执行事务队列中的所有命令,否则
    EXEC 将放弃当前事务中的所有命令。

  3. DISCARD

    回滚事务队列中的所有命令,同时再将当前连接的状态恢复为正
    常状态,即非事务状态。如果 WATCH 命令被使用,该命令将
    UNWATCH 所有的 Keys。

  4. WATCH

    在 MULTI 命令执行之前,可以指定待监控的 Keys,然而在执
    行 EXEC 之前,如果被监控的 Keys 发生修改,EXEC 将放弃执
    行该事务队列中的所有命令。

  5. UNWATCH

    取消当前事务中指定监控的 Keys,如果执行了 EXEC 或
    DISCARD 命令,则无需再手工执行该命令了,因为在此之后,
    事务中所有被监控的 Keys 都将自动取消。

Redis的事务是原子性的吗?

原子性:

数据库中的某个事务A中要更新t1表、t2表的某条记录,当事务提交,t1、t2两个表都被更新,若其中一个表操作失败,事务将回滚。

非原子性:

数据库中的某个事务A中要更新t1表、t2表的某条记录,当事务提交,t1、t2两个表都被更新,若其中一个表操作失败,另一个表操作继续,事务不会回滚。(当然对于关系型数据库不会出现非原子性)

至于Redis的事务是否为原子性,我们通过一些例子进行观察。

  1. 事务正常执行

给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>
  1. 事务失败处理(语法错误)

语法错误(编译器错误),在开启事务后,修改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的事务是原子性的吗?

  1. 事务失败处理(运行时错误)

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" 
文章目录
  1. 1. 前言
  2. 2. Redis 中事务的实现特征
  3. 3. 相关命令列表
  4. 4. Redis的事务是原子性的吗?
  5. 5. WATCH
  6. 6. DISCARD
|