事务 #
为了确保连续多个操作的原子性,一个成熟的数据库通常都会有事务支持,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 的单线程特性,它不用担心自己在执行队列的时候被其它指令打搅,可
以保证他们能得到的原子性执行。
原子性 #
> 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
QUEUED
是一个简单字符串,同 OK 是一个形式,它表示指令已经被服务器缓存到队列里了。
上面的例子是事务执行到中间遇到失败了,因为不能对一个字符串进行数学运算,事务在遇到指令执行失败后,后面的指令还继续执行,
所以 poorman
的值能继续得到设置。
到这里,应该明白 Redis 的事务根本不能算"原子性”,而仅仅是满足了事务的"隔离性”,隔离性中的串行化 —— 当前执行的事务有着不被其 它事务打断的权利。
discard(丢弃) #
discard
指令,用于丢弃事务缓存队列中的所有指令,在 exec
执行之前。
> get books
(nil)
> multi
OK
> incr books
QUEUED
> incr books
QUEUED
> discard
OK
> get books
(nil)
优化 #
上面的 Redis 事务在发送每个指令到事务缓存队列时都要经过一次网络读写,当一个事务内部的指令较多时,需要的网络 IO 时间也会线性增长。所以
通常 Redis 的客户端在执行事务时都会结合 pipeline
一起使用,这样可以将多次 IO 操作压缩为单次 IO 操作。比如 Python 的 Redis 客
户端时执行事务时是要强制使用 pipeline
的。
pipe = 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)
Redis 禁止在
multi
和exec
之间执行watch
指令,而必须在multi
之前做好盯住关键变量,否则会出错。