Redis系列(4) —— 事务

MULTI,EXEC,DISCARD和WATCH是Redis事务的基础。他们允许在一个步骤中执行一组命令,并且具有两种重要保障:

事务中的所有命令都被序列化并顺序执行。在一个Redis事务执行期间,它不会被其他客户端发出的请求所打断。这一点保证了命令作为一个隔离操作被执行。

要么所有命令都被执行,要么都不执行,所以Redis事务是也是原子性的。在一个事务中,EXEC命令会触发所有命令的执行,所以如果一个客户端在事务调用MULTI命令之前失去了与服务器的连接,那么一条操作都不会被执行,相反,如果EXEC命令被调用了,那么所有的操作会被执行。当使用AOF时,Redis会确保使用一个write(2)系统调用将事务写入磁盘。然而,如果Redis 服务端崩溃了或者是被系统管理员以某种暴力的方式停掉了服务,那么很有可能只有一部分操作被注册。Redis会在重启的时候检测到这种情况,然后退出并抛出错误。使用redis-check-aof工具可以移除这部分不完整的事务来修复AOF,这样一来服务器就可以重新启动了。

从2.2版本开始,Redis允许使用另外一种方式来保证以上两点,这是一种与CAS操作很相似的乐观锁。本文将在稍后介绍这一点。

用法

Redis使用MULTI命令开始一个事务。该命令总是返回OK。此时用户就可以发出多个命令。这些命令并不会被立即执行,而是会被放入队列中。所有的命令都会在EXEC被调用后一次性执行。

调用DISCARD命令将会清空事务队列并退出事务。

下面的示例以原子方式增加键foo和bar。

> MULTI
OK
> INCR foo
QUEUED
> INCR bar
QUEUED
> EXEC
1) (integer) 1
2) (integer) 1

从上面的会话中可以看出,EXEC返回的是一个数组,数组中的每个元素都是事务中单个命令的返回值,并且元素顺序与命令的发送顺序相同。

当Redis连接处于一个MULTI请求的上下文中时,所有命令都会使用字符串QUEUED(从Redis协议的角度来看是作为一个回复的状态进行发送)进行回复。这些被放入队列的命令将会在EXEC被调用时执行。

事务中的错误

在事务执行过程中有可能会遇到两种类型的命令错误:

  • 一个命令入队列失败,所以在EXEC被调用之前可能就会报错。例如命令可能有语法错误(参数个数不对,命令名字不对等等...),或者其它一些更严重的情形,比如说内存溢出(如果服务器使用maxmemory配置了内存限制的话)。

  • 一个命令可能在EXEC被调用后失败,例如对键值使用了错误的操作(例如针对字符串调用列表操作)。

客户端曾经会通过检查入队列的返回值来感知第一类发生在EXEC调用之前错误:如果返回值为QUEUED,那么该命令入队列成功,否则Redis会返回错误。如果在命令入队列时发生了错误,大多数客户端会丢弃它,放弃事务的执行。

然而从2.6.5版本的Redis开始,服务器将会记住命令积累过程中的错误,并且会拒绝执行事务,它在执行EXEC期间会返回错误并自动丢弃事务。

在2.6.5之前的版本中,事务执行的行为是这样的:客户端调用EXEC命令时会忽略之前的错误,仅仅执行入队列成功的命令。新的行为使用流水线使事务变得更加简单,整个事务可以一次发送,然后稍后一次读取所有返回值。

在EXEC命令之后发生的错误并没有进行特殊处理:即使有些命令在事务执行期间失败了,所有的命令也都会被执行。

从协议层的角度来看这个问题将会更加清晰。在下面这个例子中,即使语法是正确的,还是有一个命令会执行失败。

Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
MULTI
+OK
SET a 3
abc
+QUEUED
LPOP a
+QUEUED
EXEC
*2
+OK
-ERR Operation against a key holding the wrong kind of value

EXEC命令返回了一个二元体串,其中一个是OK,另一个是ERR。这是由客户端库来决定如何找到一种合适方式来向用户展示事务中的错误。

需要注意的一点是:即使某个命令执行失败了,事务中的其他命令仍然会被执行——Redis不会停止命令的处理。

下面另外一个例子,它使用了telnet协议来展示当出现语法错误时,它会尽快报告出来。

MULTI
+OK
INCR a b c
-ERR wrong number of arguments for 'incr' command

这次由于语法错误,这个INCR命令根本不会被放入队列。

为什么Redis不支持回滚

如果你有传统数据库的背景,那么这一事实将会让你感到很奇怪:Redis命令在事务执行中可能会失败,但是Redis还是会执行事务中剩下的命令而不是回滚。

然而,这种做法还是有以下好处:

  • Redis命令只会在出现语法错误(这种错误在命令入队列时并不会被检测到)或者键和对应的值类型不匹配时才会失败:这意味着在实际操作中,命令执行失败是编程错误的结果,并且这种错误通常可以在开发过程中就检测到,而不是在生产环境。

  • Redis内部的简化和更加的快速是因为它不支持回滚。

有一种观点认为Redis的这种机制会产生bug,然而我们应该注意到,通常来说,回滚并不能将你从编程错误中挽救出来。举个栗子:如果你本来想对一个键增加1,却不小心加了2,或者对错误的键进行了加1操作,此时没有任何回滚机制可以派上用场。考虑到没有人可以将程序员从他/她的编程错误中挽救出来,并且Redis中这种类型的错误很难进入生产环境,我们选择了不支持对错误进行回滚这种更加简化和快速的方式。

丢弃命令队列

DISCARD命令可以用于丢弃事务。在这种情况下,没有命令会被执行,而且连接状态也会恢复正常。

> SET foo 1
OK
> MULTI
OK
> INCR foo
QUEUED
> DISCARD
OK
> GET foo
"1"
使用CAS的乐观锁

WATCH命令用来为Redis事务提供一种CAS行为。

被WATCH的键将会被监控是否有变化。只要有一个被监控的键在EXEC命令执行前被修改了,那么整个事务都会被丢弃,EXEC命令会返回空,以此来提示事务执行失败了。

例如,如果我们需要原子性地为一个键增加1(假设Redis没有INCR命令),我们的第一次尝试可能是这样的:

val = GET mykey
val = val + 1
SET mykey $val

只有在只有一个客户端执行这个操作时,上述代码的执行结果才是可信的。如果有多个客户端几乎同时尝试给该键增加1,那么这里就会产生竞争条件。比如所,客户端A和B会读到旧的值10,然后两者都将该值增加至11,最后将11赋值给原来的键。这样一来最后键值会是11而不是12.

多亏有了WATCH命令,使得我们可以很好地解决这个问题:

WATCH mykey
val = GET mykey
val = val + 1
MULTI
SET mykey $val
EXEC

使用上述代码,如果有竞争条件,即有另外一个客户端在我们调用WATCH和EXEC期间需改了val的值,那么这个事务将失败。

我们需要做的就是重复这个操作,希望这次不会有新的竞争条件。这种形式的锁被称为乐观锁,这是一种功能非常强大的锁形式。在很多应用场景中,多个客户端会访问不同的键,所以碰撞并不会经常发生,我们也就不需要进行重试。

了解WATCH

所以WATCH到底是什么?它是一个可以让我们有条件地执行EXEC的命令:当我们向Redis请求一个事务时,只有所有被监控的键都没有被修改,事务才会执行(但是在这个客户端的该事务内部可能会修改被监控的键,这种情况并不会导致事务被丢弃。了解更多详情请访问https://github.com/antirez/redis-doc/issues/734),否则事务将会被丢弃。(有一点需要注意的是如果你监控了一个易变的键,并且当你开始监控它后,该键过期了,此时事务还是会执行。了解更多详情请访问http://code.google.com/p/redis/issues/detail?id=270)

WATCH命令可以被多次调用。所有的WATCH调用都会监控从WATCH命令到EXEC命令之间的键值变化。所以你也可以对一个WATCH命令发送任意数目的命令。

当EXEC命令被调用后,所有的键将不再会被监控,无论最终事务是否执行成功。当客户端连接被关闭时,所有之前被监控的键也将不再会被监控。

使用无参数的UNWATCH命令可以取消对所有键的监控。有时候当我们使用乐观锁锁定多个键时这个命令会非常有用。我们开启了一个事务去修改这些键,但是当我们读完键内容后又不想继续处理了,这种情况下我们就可以调用UNWATCH命令,然后就可以自由开启新的事务了。

使用WATCH实现ZPOP

一个很好的用来解释WATCH如何用来创建一个新的原子操作的例子是实现ZPOP,这是一个以原子方式从有序集合中弹出较低分值元素的命令。如下是它的最简单实现:

WATCH zset
element = ZRANGE zset 0 0
MULTI
ZREM zset element
EXEC

如果EXEC调用失败(例如返回为空),上述代码就会进行重试。

Redis脚本和事务

从定义上来说,Redis脚本本身就是一个事务,所以任何可以用事务实现的功能,都可以用脚本来实现,并且通常来说使用脚本会更加简单,速度也更快。

这种功能的重复是因为这样一个事实:脚本是在Redis 2.6中才引进的,而事务在很久之前的版本中就已经存在。然而短期内我们并不想移除redis对事务的支持,因为从语义上来说它也是合适的,即使不借助Redis脚本我们依然可以避免竞争条件,更何况Redis事务的实现复杂度是最小的。

然而在不久的将来我们可能会看到所有用户都只使用脚本,这种情况也不是不可能发生。如果真的这样,我们将会废弃并最终移除事务功能。

2017-09-10

原文链接:https://redis.io/topics/transactions

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,014评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,796评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,484评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,830评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,946评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,114评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,182评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,927评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,369评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,678评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,832评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,533评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,166评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,885评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,128评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,659评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,738评论 2 351

推荐阅读更多精彩内容

  • redis事务 Redis 通过 MULTI 、 DISCARD 、 EXEC 和 WATCH 四个命令来实现事务...
    全能程序猿阅读 2,159评论 0 11
  • Redis 通过 MULTI 、 DISCARD 、 EXEC 和 WATCH 四个命令来实现事务功能, 本章首先...
    binge1024阅读 519评论 0 2
  • redis的事务 严格意义来讲,redis的事务和我们理解的传统数据库(如mysql)的事务是不一样的。 redi...
    jsondream阅读 29,876评论 5 36
  • 本文将从Redis的基本特性入手,通过讲述Redis的数据结构和主要命令对Redis的基本能力进行直观介绍。之后概...
    kelgon阅读 61,134评论 23 626
  • 画画的时候觉得时间过得真快 貌似实物比图片要好看些,哈哈
    清晓远阅读 365评论 3 2