Redis 事务

Redis 事务

事务的使用

Redis 在形式上分别是 multi/exec/discardmulti 指示事务的开始,exec 指示事务的执行,discard 指示事务的丢弃。

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

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

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

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

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

不满足原子性

事务的原子性是指要么事务全部成功,要么全部失败,那么 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 的事务根本不能算「原子性」,而仅仅是满足了事务的「隔离性」,隔离性中的串行化——当前执行的事务有着不被其它事务打断的权利

  • 命令入队时就报错,会放弃事务执行,保证原子性;
  • 命令入队时没报错,实际执行时报错,不保证原子性;
  • EXEC命令执行时实例故障,如果开启了AOF日志,可以保证原子性。

原子性的情况比较复杂,只有当事务中使用的命令语法有误时,原子性得不到保证,在其它情况下,事务都可以原子性执行。

一致性

在命令执行错误或Redis发生故障的情况下,Redis事务机制对一致性属性是有保证的。

持久性

因为Redis是内存数据库,所以,数据是否持久化保存完全取决于Redis的持久化配置模式。

如果Redis没有使用RDB或AOF,那么事务的持久化属性肯定得不到保证。如果Redis使用了RDB模式,那么,在一个事务执行后,而下一次的RDB快照还未执行前,如果发生了实例宕机,这种情况下,事务修改的数据也是不能保证持久化的。

如果Redis采用了AOF模式,因为AOF模式的三种配置选项no、everysec和always都会存在数据丢失的情况,所以,事务的持久性属性也还是得不到保证。

所以,不管Redis采用什么持久化模式,事务的持久性属性是得不到保证的。

使用管道优化

上面的 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()

管道特点

管道将一组 Redis 命令进行组装,通过一次 RTT 传输给 Redis,再将这组 Redis 命令的执行结果按照顺序返回给客户端。

原生批量命令 vs 管道 ?

  • 原生批量是原子的,管道是非原子的。
  • 原生批量是一个命令对应多个 Key,管道支持多个命令。
  • 原生批量是 Redis 服务器端实现的,管道需要服务端和客户端共同实现。

虽然 Pipeline 只能操作一个 Redis 实例,但是在分布式场景中,也可以用来作为批量操作的重要优化手段。