为了确保连续多个操作的原子性,一个成熟的数据库通常都会有事务支持,Redis也不例外。Redis的事务使用非常简单,不同于关系数据库,我们无需理解那么多复杂的事务模型了,就可以直接使用。不过也正是因为这种简单性,它的事务模型很不严格,这要求我们不能像使用关系数据库的事务一样使用Redis。
Redis事务的基本使用
每个事务的操作都有begin、commit、和rollback、begin指示事务的开始,commit指示事务的提交,rollback指示事务的回滚。
Redis与其差不多,对应的分别是multi/exec/discard。multi指示事务的开始,exec指示事务的执行,discard指示事务的丢弃。
1 | >multi |
上面的指令演示了一个完整的事务过程,所有的指令在exec之前不执行,而是缓存在服务器的一个事务队列中,服务器一旦受到exec指令,就开始执行整个事务队列,执行完毕后一次性返回所有指令的运行结果。因为Redis的单线程特性,它不用担心自己在执行队列的时候被其他指令打搅,可以保证他们得到的[原子性]执行。
上图显示了以上事务过程完整的交互结果。QUEUED是一个简单字符串,同OK是一个形式,它表示指令已经被服务器缓存到队列里了。
原子性
事务的原子性是指要么事务全部成功,要么全部失败,那么Redis事务的执行时原子性的么?
下面我们来看一个特别的例子。
1 | >multi |
上面的例子是事务执行到中间遇到失败了,因为我们不能对一个字符串进行数学运算,事务在遇到指令执行失败后,后面的指令还继续执行,所以poorman的值能继续得到设置。
到这里,应该明白Redis的事务根本不能算[原子性],而仅仅是满足了事务的[隔离性],隔离性中的串行化–当前执行的事务有着不被其他事务打断的权利。
discard(丢弃)
Redis为事务提供了一个discard指令,用于丢弃事务缓存队列中的所有指令,在exec执行之前。
优化
上面Redis事务在发送每个指令到事务缓存队列时都要经过一次网络读写,当一个事务内部的指令较多时,需要的网络IO时间也会线性增长。所以通常Redis的客户端在执行事务时都会结合pipeline一起使用,这样可以将多次IO操作压缩为单次IO操作。
Watch
考虑到一个业务场景,Redis存储了我们的账户余额数据,它是一个整数。现在有两个并发的客户端要对账户余额进行修改操作,这个修改不是简单的incrby指令,而是要对余额乘以一个倍数。Redis可没有提供multiplyby这样的指令。我们需要先取出余额然后在内存里乘以倍数,再将结果写回到Redis。
这就会出现并发问题,因为有多个客户端会并发进行操作。我们可以通过Redis的分布式锁来避免冲突,这是一个很好的解决方案。分布式锁是一种悲观锁,那是不是可以使用悲观锁的方式来解决冲突呢?
Redis提供了watch的机制,它就是一种悲观锁。有了watch我们又多了一种可以用来解决并发修改的方法。watch的使用方式如下:1
2
3
4
5
6
7
8
9
10while True:
do_watch()
commands()
multi()
send_commands()
try:
exec()
break
except WatchError:
continue
watch会在事务开始之前盯住一个或多个关键变量,当事务执行时,也就是服务受到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之前做好盯住关键变量,否则会出错。