事务

事务 #

为了确保连续多个操作的原子性,一个成熟的数据库通常都会有事务支持,Redis 也支持简单的事务模型。

使用 #

每个事务的操作都有 begincommitrollback 三个指令:

  • 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 禁止在 multiexec 之间执行 watch 指令,而必须在 multi 之前做好盯住关键变量,否则会出错