Redis 事务
事务的使用
Redis 在形式上分别是 multi
/exec
/discard
。multi
指示事务的开始,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
之后,队列中的所有指令都没执行,就好像 multi
和 discard
中间的所有指令从未发生过一样。
不满足原子性
事务的原子性是指要么事务全部成功,要么全部失败,那么 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 实例,但是在分布式场景中,也可以用来作为批量操作的重要优化手段。