Redis 事务

原理

为了确保连续多个操作的原子性,
一个成熟的数据库通常都会有事务支持,
Redis 也不例外。
Redis 的事务使用非常简单,
不同于关系数据库,
我们无须理解那么多复杂的事务模型,
就可以直接使用。
不过也正是因为这种简单性,
它的事务模型很不严格,
这要求我们不能像使用关系数据库的事务一样来使用 Redis。

Redis 事务的基本使用

每个事务的操作都有 begin、commit 和 rollback,
begin 指示事务的开始,commit 指示事务的提交,rollback 指示事务的回滚。
它大致的形式如下。

begin();
try {
command1();
command2();
....
commit();
} catch(Exception e) {
rollback();
}

Redis 在形式上看起来也差不多,
分别是 multi/exec/discard。
multi 指示事务的开始,
exec 指示事务的执行,
discard 指示事务的丢弃。

>multi
OK
> incr books
QUEUED
> incr books
QUEUED
> exec
(integer) 1
(integer) 2

上面的指令演示了一个完整的事务过程,
所有的指令在 exec 之前不执行,
而是缓存在服务器的一个事务队列中,
服务器一旦收到 exec 指令,
才开执行整个事务队列,
执行完毕后一次性返回所有指令的运行结果。
因为 Redis 的单线程特性,
它不用担心自己在执行队列的时候被其它指令打搅,
可以保证他们能得到的「原子性」执行。

QUEUED 是一个简单字符串,
同 OK 是一个形式,
它表示指令已经被服务器缓存到队列里了。

原子性

事务的原子性是指要么事务全部成功,
要么全部失败,
那么 Redis 事务执行是原子性的么?

下面我们来看一个特别的例子。
> multi
OK
> set books iamastring
QUEUED
> incr books
QUEUED
> set poorman iamdesperate
QUEUED
> exec
1) OK
2) (error) ERR value is not an integer or out of range
3) OK
> get books
"iamastring"
> get poorman
"iamdesperate

上面的例子是事务执行到中间遇到失败了,
因为我们不能对一个字符串进行数学运算,
事务在遇到指令执行失败后,
后面的指令还继续执行,
所以 poorman 的值能继续得到设置。

到这里,你应该明白 Redis 的事务根本不能算「原子性」,
而仅仅是满足了事务的「隔离性」,
隔离性中的串行化——当前执行的事务有着不被其它事务打断的权利。

discard(丢弃)

Redis 为事务提供了一个 discard 指令,
用于丢弃事务缓存队列中的所有指令,
在 exec 执行之前。

> get books
(nil)
> multi
OK
> incr books
QUEUED
> incr books
QUEUED
> discard
OK
> get books
(nil)

我们可以看到 discard 之后,
队列中的所有指令都没执行,
就好像 multi 和 discard 中间的所有指令从未发生过一样。

优化

上面的 Redis 事务在发送每个指令到事务缓存队列时都要经过一次网络读写,
当一个事务内部的指令较多时,
需要的网络 IO 时间也会线性增长。
所以通常 Redis 的客户端在执行事务时都会结合 pipeline 一起使用,
这样可以将多次 IO 操作压缩为单次 IO 操作。
比如我们在使用 Python 的 Redis 客户端时执行事务时是要强制使用 pipeline 的。

pypipe = redis.pipeline(transaction=true)
pipe.multi()
pipe.incr("books")
pipe.incr("books")
values = pipe.execute()

Watch

考虑到一个业务场景,
Redis 存储了我们的账户余额数据,
它是一个整数。
现在有两个并发的客户端要对账户余额进行修改操作,
这个修改不是一个简单的 incrby 指令,
而是要对余额乘以一个倍数。
Redis 可没有提供 multiplyby 这样的指令。
我们需要先取出余额然后在内存里乘以倍数,
再将结果写回 Redis。

这就会出现并发问题,
因为有多个客户端会并发进行操作。
我们可以通过 Redis 的分布式锁来避免冲突,
这是一个很好的解决方案。
分布式锁是一种悲观锁,
那是不是可以使用乐观锁的方式来解决冲突呢?

Redis 提供了这种 watch 的机制,
它就是一种乐观锁。
有了 watch 我们又多了一种可以用来解决并发修改的方法。
watch 的使用方式如下:

while True:
do_watch()
commands()
multi()
send_commands()
try:
exec()
break
except WatchError:
continue

watch 会在事务开始之前盯住 1 个或多个关键变量,
当事务执行时,
也就是服务器收到了 exec 指令要顺序执行缓存的事务队列时,
Redis 会检查关键变量自 watch 之后,
是否被修改了 (包括当前事务所在的客户端)。
如果关键变量被人动过了,
exec 指令就会返回 null 回复告知客户端事务执行失败,
这个时候客户端一般会选择重试。

> watch books
OK
> incr books # 被修改了
(integer) 1
> multi
OK
> incr books
QUEUED
> exec # 事务执行失败
(nil)

当服务器给 exec 指令返回一个 null 回复时,
客户端知道了事务执行是失败的,
通常客户端 (redis-py) 都会抛出一个 WatchError 这种错误,
不过也有些语言 (jedis) 不会抛出异常,
而是通过在 exec 方法里返回一个 null,
这样客户端需要检查一下返回结果是否为 null 来确定事务是否执行失败。

注意事项
Redis 禁止在 multi 和 exec 之间执行 watch 指令,
而必须在 multi 之前做好盯住关键变量,
否则会出错。

思考题

为什么 Redis 的事务不能支持回滚?

本文转载自 Redis 深度历险:核心原理与应用实践 - 老錢 - 掘金小册

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

推荐阅读更多精彩内容