一、概述

Redis和众多其它数据库(MySQL、Oracle、DB2、PostgreSQL)一样,Redis作为NoSQL数据库也同样提供了事务机制。

在Redis中,MULTI,EXEC,DISCARD,WATCH 这四个命令是我们实现事务的基石。

相信对有关系型数据库开发经验的开发者而言这一概念并不陌生,即便如此,我们还是会简要的列出Redis中事务的实现特征:

1)在事务中的所有命令都将会被串行化的顺序执行,事务执行期间,Redis不会再为其它客户端的请求提供任何服务,从而保证了事物中的所有命令被原子的执行。

2)Redis和关系型数据库中的事务相比,在Redis事务中如果有某一条命令执行失败,其后的命令仍然会被继续执行。

3)我们可以通过 MULTI 命令开启一个事务,有关系型数据库开发经验的人可以将其理解为 "BEGIN TRANSACTION" 语句。在该语句之后执行的命令都将被视为事务之内的操作,最后我们可以通过执行 EXECDISCARD 命令来提交、回滚该事务内的所有操作。这两个Redis命令可被视为等同于关系型数据库中的 COMMITROLLBACK 语句。

4)在事务开启之前,如果客户端与服务器之间出现通讯故障并导致网络断开,其后所有待执行的语句都将不会被服务器执行。如果网络中断事件是发生在客户端执行EXEC命令之后,那么该事务中的所有命令都会被服务器执行。

5)当使用 Append-Only 模式时,Redis会通过调用系统函数write将该事务内的所有写操作在本次调用中全部写入磁盘。如果在写入的过程中出现系统崩溃,如电源故障导致的宕机,那么此时也许只有部分数据被写入到磁盘,而另外一部分数据却已经丢失。Redis服务器会在重新启动时执行一系列必要的一致性检测,一旦发现类似问题,就会立即退出并给出相应的错误提示。此时,我们就要充分利用Redis工具包中提供的 redis-check-aof 工具,该工具可以帮助我们定位到数据不一致的错误,并将已经写入的部分数据进行回滚。修复之后我们就可以再次重新启动Redis服务器了。

 

二、相关命令列表

命令原型 时间复杂度 命令描述 返回值
MULTI   用于标记事务的开始,其后执行的命令都将被存入命令队列,直到执行EXEC时,这些命令才会被原子的执行。 始终返回OK
EXEC   执行在一个事务内命令队列中的所有命令,同时将当前连接的状态恢复为正常状态,即非事务状态。如果在事务中执行了WATCH命令,那么只有当WATCH所监控的Keys没有被修改的前提下,EXEC命令才能执行事务队列中的所有命令,否则EXEC将放弃当前事务中的所有命令。 原子性的返回事务中各条命令的返回结果。如果在事务中使用了WATCH,一旦事务被放弃,EXEC将返回NULL-multi-bulk回复。
DISCARD   回滚事务队列中的所有命令,同时再将当前连接的状态恢复为正常状态,即非事务状态。如果WATCH命令被使用,该命令将UNWATCH所有的Keys。 始终返回OK。

WATCH 

key [key ...]

O(1) 在MULTI命令执行之前,可以指定待监控的Keys,然而在执行EXEC之前,如果被监控的Keys发生修改,EXEC将放弃执行该事务队列中的所有命令。 始终返回OK。
UNWATCH O(1) 取消当前事务中指定监控的Keys,如果执行了EXEC或DISCARD命令,则无需再手工执行该命令了,因为在此之后,事务中所有被监控的Keys都将自动取消。 始终返回OK。

 

三、命令示例:

1. 事务被正常执行

#在Shell命令行下执行Redis的客户端工具。

/> redis-cli

#在当前连接上启动一个新的事务。

redis 127.0.0.1:6379> multi

OK

#执行事务中的第一条命令,从该命令的返回结果可以看出,该命令并没有立即执行,而是存于事务的命令队列

redis 127.0.0.1:6379> incr t1

QUEUED

#又执行一个新的命令,从结果可以看出,该命令也被存于事务的命令队列。

redis 127.0.0.1:6379> incr t2

QUEUED

    #执行事务命令队列中的所有命令,从结果可以看出,队列中命令的结果得到返回。

redis 127.0.0.1:6379> exec

1) (integer) 1

2) (integer) 1

    

2. 事务中存在失败的命令

#开启一个新的事务。

redis 127.0.0.1:6379> multi

OK

#设置键str的值为string类型的3。

redis 127.0.0.1:6379> set str 3

QUEUED

#从键str所关联的值的头部弹出元素,由于该键值 "str" 是字符串类型,而lpop命令仅能用于List类型,因此在执行exec命令时,该命令将会失败。

redis 127.0.0.1:6379> lpop str

QUEUED

#再次设置键str的值为字符串4。

redis 127.0.0.1:6379> set str 4

QUEUED

#获取键str的值,以便确认该值是否被事务中的第二个set命令设置成功。

redis 127.0.0.1:6379> get str

QUEUED

#从结果中可以看出,事务中的第二条命令lpop执行失败,而其后的set和get命令均执行成功,这一点是Redis的事务与关系型数据库中的事务之间最为重要的差别。在Redis事务中,如果有某一条命令执行失败,其后的命令仍然会被继续执行。

redis 127.0.0.1:6379> exec

1) OK

2) (error) ERR Operation against a key holding the wrong kind of value

3) OK

4) "4"

  

3. 回滚事务

#为键t2设置一个事务执行前的值。

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

#查看键t2的值,从结果中可以看出该键的值仍为事务开始之前的值。

redis 127.0.0.1:6379> get t2

"tt"

 

 

四、WATCH命令和基于CAS的乐观锁:

在Redis的事务中,WATCH命令可用于提供CAS(check-and-set)功能。

假设我们通过WATCH命令在事务执行之前监控了多个Keys,倘若在WATCH之后有任何Key的值发生了变化,EXEC命令执行的事务都将被放弃,同时返回Null multi-bulk 应答以通知调用者事务执行失败。

例如,我们再次假设Redis中并未提供incr命令来完成键值的原子性递增,如果要实现该功能,我们只能自行编写相应的代码。其伪码如下:

val = GET mykey

val = val + 1

SET mykey $val

以上代码只有在单连接的情况下才可以保证执行结果是正确的,因为如果在同一时刻有多个客户端在同时执行该段代码,那么就会出现多线程程序中经常出现的一种错误场景--竞态争用(race condition)。比如,客户端A和B都在同一时刻读取了mykey的原有值,假设该值为10,此后两个客户端又均将该值加一后set回Redis服务器,这样就会导致mykey的结果为11,而不是我们认为的12。

为了解决类似的问题,我们需要借助WATCH命令的帮助,见如下代码:

WATCH mykey

val = GET mykey

val = val + 1

MULTI

SET mykey $val

EXEC

和此前代码不同的是,新代码在获取mykey的值之前,先通过WATCH命令监控了该键,此后又将set命令包围在事务中,这样就可以有效的保证每个连接在执行EXEC之前,如果当前连接获取的mykey的值被其它连接的客户端修改,那么当前连接的EXEC命令将执行失败。这样调用者在判断返回值后就可以获悉val是否被重新设置成功。

 

 

参考推荐

Redis 常用命令

Redis系列(2)—— 概述

Redis系列(3)—— 数据结构

Redis系列(4)—— 高级功能

Redis系列(5)—— 排序与订阅

Redis实例(8)—— 事务

Redis实例(10)—— 持久化

Redis实例(11)—— 虚拟内存

Redis实例(12)—— 管线

Redis实例(13)—— 服务器管理

Redis实例(14)—— 内存优化

Redis,MemCached,MongoDB概述

Redis 核心知识图谱

单机开启多个 Redis 实例

Redis 主从集群配置高可用技术方案

BloomFilter + Redis 大数据去重策略的实现

Python 操作 redis 接口函数

Python 操作 Redis 数据库的函数

Redis 双主备份实现

PHP-redis 中文文档

php-redis 各种函数中文手册

统计Redis中各种数据的大小