[译] Redis 事务

文章来自 Redis.io

事务

MULTI, EXEC, DISCARD, WATCH 是 Redis 事务的基础。它使得一组命令可以一步执行,并提供以下两点保证:

  • 事务中的全部命令都是串行、顺序执行的。来自其他客户端的请求不会打断正在运行的事务代码。这保证了事务里的命令独立运行。

  • 所有命令要么全被执行,要么一个也不执行,因此 Redis 事务也具有原子性。EXEC 命令触发执行所有事务命令。如果客户端在调用 MULTI 命令之前丢失连接,不会执行任何命令;同理,如果执行了EXEC, 那么所有命令都会被执行。如果使用了 append-only 文件,Redis 会调用一次 write(2) 系统函数来把本次事务持久化到磁盘。但是,如果 Redis 服务器崩溃或被硬件中断,那就有可能只有部分命令被记录下来。Redis 会在重启时检查这种情况,如果属实,它会报错并退出。使用 redis-check-aof 工具可以修复 append-only 文件,删除不完整的事务命令,使得服务器正常重启。

从 Redis-2.2 开始, Redis 额外提供了另一层保护,提供一种类似于 check-and-set(CAS) 操作的乐观锁机制。

用法

MULTI 标志事务的开始。这个命令总是回复 OK。之后用户可以指定多个命令。Redis 并不会立即执行它们,而是将其入队。执行EXEC后,所有命令立即被执行。
DISCARD 命令则清空整个队列,退出事务。

下面例子将 foo 和 bar 指示的值原子自增。

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

从上面可以看出, EXEC 返回一系列回复。每个响应都是单个命令的回复,顺序也跟命令的顺序一致。
当 Redis 连接处于 MULTI 请求过程时,所有的命令都以 QUEUED 响应。入队命令待由 EXEC 后执行。

事务中的错误

事务有两种可能的错误:

  • 命令入队失败,因此它是 EXEC 执行之前的错误。如,命令语法有错(参数数量不对、命令名称不对等等),或者处于紧急情况,像内存溢出(如果服务器配置了 maxmemory)。

  • 调用 EXEC 后出错。如,对一个键执行错误类型的值操作(对字符串键执行列表命令)。

在执行 EXEC 之前,客户端可以通过命令的响应来察觉第一种错误: 如果返回 QUEUED, 说明正确入队; 否则返回错误信息。如果入队过程中出错,大多数 Redis 客户端终止这个事务。

但从 Redis-2.6.5 开始,服务器会记录手机命令时的错误,在执行 EXEC 时拒绝执行事务,自动丢弃这个事务。

Redis-2.6.5 之前的行为是,事务继续执行其他正确入队的命令,尽管 EXEC 发生过错误。后来的新行为使得 Redis 更容易将事务与流水线相结合。如此,整个事务可以一次性发送出去,一次性接受回复。

发生在 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 仍然执行了其他正确入队的命令,即,Redis 并不是啥都没做。

再来一个例子,仍使用 Telnet 连接,表明尽可能快地报告语法错误。

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

由于语法错误, INCR 命名没有入队。

为什么 Redis 不支持回滚?

如果你有过关系型数据库的使用经验,可能会对 Redis 事务出错但仍执行部分命令的行为感到奇怪。

现在来理解一下这个行为:

  • Redis 命令出错的两种情况:语法不对(而且这种错误不会在入队过程中被发现),和执行类型不匹配的操作:也就是说,错误的命令通常是编程错误导致的,这种行为完全可以在开发环境下检查到,不会涉及生产环境。
  • Redis 天生简洁、快速,不需要回滚功能。

反对 Redis 不支持回滚的观点之一是说,它会带来bugs。但是,请注意,回滚并不能帮你避免编程错误。比如,一条语句将值自增2,而不是1,或者对错误的键执行自增,那么没有任何回滚机制能帮得上忙。鉴于没有办法完全保证免于编程错误,以及出错的 Redis 命令不可能进入生产环境,我们选择了不支持回滚功能,以实现更简单更快的数据库。

丢弃命令队列

DISCARD 可以用来终止一个事务。这样一来,不执行任何命令,连接状态转向正常。

> SET foo 1
OK
> MULTI
OK
> INCR foo
QUEUED
> DISCARD
OK
> GET foo
"1"

使用 check-and-set 的乐观锁

WATCH 为 Redis 事务提供了 check-and-set 行为支持。

被 WATCH 的键会被监测其可能的修改。如果在执行 EXEC 之前,至少有一个被 WATCH 的键被修改,那么整个事务将失效。EXEC 返回空回复告知事务失败。

比如我们需要原子性地自增一个键(假设还没有 INCR 这个命令)。

一开始可能会这么做

val = GET key
val = val + 1
SET key $val

这种做法只有当一个客户端在一定时间内执行时才有效。如果多用户几乎同时对这个键执行自增操作,将会产生竞态(race condition)。比如,客户端 A 和 B 都读到旧值 10。两个人都将这个值自增到 11。最终把新值赋给这个键。因此最这个键的值是11,而不是预期的12。

幸亏有 WATCH 命令,我们可以处理这一问题

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

如果竞态发生,另一个客户端在 WATCHEXEC 之间修改了 key 的值,那么整个事务将失败。

重复这个操作,期望不产生新的竞态。这种锁称作乐观锁,在锁机制中很有用。在许多应用场景中,多客户端访问不同的键,因此冲突不太频繁发生,通常也就不需要重复执行这个操作。

解释 WATCH

到底什么是 WATCH 呢?这个命令给 EXEC 加了一层限制:限制 Redis 只有当被监测的键没有被修改时才执行事务。否则不会进入事务。(注意,如果你 WATCH 一个有过期时间的键,并在 WATCH 之后过期了,那么 EXEC 仍会有效,即事务不会失败)。

WATCH 可以多次使用。WATCH 的生命周期从被调用开始,到 EXEC 执行时结束。你可以一次给 WATCH 传多个键。

当执行 EXEC 后,所有的键都处于 UNWATCHED 状态,不管事务成功失败与否。当客户端连接断开时,所有的键也归为 UNWATCHED。

也可以用 WATCH (不带参数)来清空所有被监视的键,即取消所有被监视状态。如果我们一开始监视了一些键,自己想修改它们,而对其加上乐观锁,但后来改主意了,这时候执行 UNWATCH 来把连接置于正常状态,以便开启新的事务。

使用 WATCH 实现 ZPOP

实现 ZPOP 是解释 WATCH 用来创建原子操作的好例子。这个命令原子地从有序集合中弹出分值(score)最低的元素。下面是最简单的一种实现。

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

如果 EXEC 失败了(返回空回复),重复执行。

Redis 脚本和事务

从定义上讲, Redis 脚本也具有事务性。所以通常对事务的操作也可以施加于脚本之上。脚本更简单,执行起来也更快。

之所以有这部分,是因为脚本在 Redis-2.6 才引入,而事务存在已久。但是短期内不会移除对事务的支持因为即使不借助 Redis 脚本,避免竞态也是有可能的,尤其是 Redis 事务的实现复杂性已经最低了。

但是,很有可能不久的将来, Redis 用户群只使用脚本。如果真是这样,我们会考虑把事务功能移除。

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

推荐阅读更多精彩内容

  • redis事务 Redis 通过 MULTI 、 DISCARD 、 EXEC 和 WATCH 四个命令来实现事务...
    全能程序猿阅读 2,154评论 0 11
  • MULTI、EXEC、DISCARD和WATCH命令是Redis事务功能的基础。Redis事务允许在一次单独的步骤...
    金星show阅读 488评论 0 0
  • Redis 通过 MULTI 、 DISCARD 、 EXEC 和 WATCH 四个命令来实现事务功能, 本章首先...
    binge1024阅读 519评论 0 2
  • 五种数据结构简介 Redis是使用C编写的,内部实现了一个struct结构体redisObject对象,通过结构体...
    彦帧阅读 6,931评论 0 14
  • 我在学校里学会了长和短,知道许多汉字,而且也认识了很多东西。学校有小朋友,都热情。老师说只要是得了十个小红花...
    倚楼听雨_81b3阅读 208评论 0 2