40:Redis的管道与事务

193 阅读5分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。


1 管道

管道的原理很简单,就是把多次请求合并为一次发送。但是有一点需要注意,管道(Pipeline)本身并不是Redis服务器直接提供的技术,而是由客户端提供的,和服务器没有关系。一般各个Redis的工具Jar包中都提供了管道了相关方法。

下图是执行一次指令所产生的交互,客户端发请求过去,Redis返回响应:

在这里插入图片描述

那两条指令就是这样的了

在这里插入图片描述

那对于客户端来说经历的是:

在这里插入图片描述

但是如果我们改成这样结果也不会受到影响,而且服务端也并没有区别对待,同样是接到一条指令,然后执行完将结果发送回去:

在这里插入图片描述

那改成这样会有好处吗?来看一下请求交互的本质:

在这里插入图片描述

客户端和服务器都是将数据写入sendBuffer,然后从receiveBuffer里面读数据。但是注意,客户端写入sendBuferr(也就是write操作)之后并不停在那里等着receiveBuffer(也就是read操作)的数据,而是写入之后就立刻返回做自己的事情了,剩下的事情就由操作系统处理了,read操作不是从目标机器拉取数据,而是等待数据有数据到达receiveBuffer后直接读取。write的操作很快,耗费不了多少时间,因此客户端执行一条指令的主要耗时就在read等待数据的这段时间了,也就是write之后,等待数据发送到服务端的receiveBuffer,然后服务端read之后写到服务端的sendBuffer,再发送到客户端的receiveBuffer回来这段时间。

这时间再看上面两条指令的步骤,就变成了这样的:

在这里插入图片描述

和这样的:

在这里插入图片描述

管道的本质就是这样,他并不是服务端的什么特性,而是通过改变读写顺序,将多个指令等待响应的时间整合成一个,以此带来巨大的性能提升。 指令越多,带来的效果越明显,当然并不是没有限制的,毕竟服务端本身的处理能力也是有瓶颈的。

2 事务

为了确保原子性,成熟的数据库都有事务机制支持,事务拥有ACID的特性。Redis也有类似的事务机制,但是Redis事务机制相当有限,严格意义上来说甚至称不上事务。

2.1 multi/exec/discard

数据库事务上的begin/commit/rollback对应redis事务中的multi/exec/discard。multi表示事务开始,exec表示执行事务,discard表示丢弃事务。

指令上的使用:

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

这就是一个事务的使用过程,纳入事务的指令在exec之前不执行,而是缓存在服务器上的一个事件队列中,QUEUED标识放入成功,服务器收到exec指令后,执行整个事件队列,然后一次性返回结果,因为Redis的单线程特性,所以不会有打扰,看似是保证了原子性。。为什么是看似呢?接下来再看:

>multi
OK
>set name zhangsan
QUEUED
>incr name
QUEUED
>set name lisi
QUEUED
>exec
1) OK
2)(error) ERR value is not an integer or out of range
3)OK
>get name
"lisi"

因为现将name设置成字符串,不能自增,很明显incr name这个指令出错了,但是后续的set name lisi却依然执行了。也就是说事务中的某一条指令出错,后续的指令依然会继续执行,而且成功了,这也是为什么说Redis事务只是看似保证了原子性。Redis的事务仅仅是保证这些指令执行过程中不会被打断而已。

Redis事务可以通过discard丢弃:

>multi
OK
>incr name
QUEUED
>incr name
QUEUED
>discard
OK

这样,这个事务的指令就不是执行而是被丢弃了,事务也结束了。

结合上一部分的管道知识,是不是发现管道和事务很配,因此使用事务时一般执行事务时都会搭配管道一起使用,将多次IO的等待时间整合成一次,提高性能。Python的Redis客户端执行事务更是强制要求使用管道。

2.2 watch

有这样一个场景,需要先获得redis内的值,然后根据这个值作出计算再重新赋值进去。如果是加算,比如说存款新增100,那可以用incrby完成,但是如果是乘算呢?Redis可没有multiplyby这样的指令,此时就需要先取,再算,再赋值几个步骤。此时就会有并发问题,显然multi/exec解决不了这个场景,毕竟multi/exec只保证指令集一起执行,但是不保证你塞指令时其它指令不会操作。 或许可以使用加锁的方案,这确实是一种悲观锁的解决方案,那有没有乐观锁的解决方案呢?此时就需要watch出马了。 watch会在事务开始前盯住一个或多个变量,当事务执行时,也就是服务器收到exec指令时,Redis会先检查watch关注的变量在watch之后是否被修改过,如果被修改过,那么exec指令就不会执行指令集,而且返回null告知调用者:

>watch name
OK
>incr name
OK
>multi
OK
>incr name
QUEUED
>exec
(nil)

此时调用者就可以根据此返回决定采用何种方式处理(重试还是抛异常)。而在客户端方面,返回可以有所不同,redis-py是抛出WatchError的错误,Jedis返回的是null。 但是有一点要注意,watch要在multi之前执行。Redis禁止watch在multi和exec之间执行,这种场景会报错。

5.3 为什么Redis事务不支持回滚呢

Redis不支持回滚是出于导致事务失败的原因及保证性能考虑的,Redis指令失败仅会是错误的语法导致的,这些都是编程时可以避免的,对于错误的语法即使回滚也没有任何用处,这种错误应该在上线之前被发现。且不支持这种没有必要的回滚机制还能保持Redis内部的简单和快捷。


开发成长之旅 [持续更新中...]
欢迎关注…

参考资料:
《Redis深度历险》