前言首先,我们来看一下Redis客户端和服务端的交互模型。可以得出结论:1.Redis是基于一个Request和一个Response的同步请求服务2.客户端将数据包发送到服务器,然后服务器将响应数据发回给客户端。这段时间称为往返时间RTT(RoundTripTime)。当客户端需要连续执行很多请求时,很容易看出往返时间对系统性能的影响。例如:如果往返时间RTT为250毫秒,即使Redis服务器每秒可以处理1000个请求,我们每秒也最多只能处理4个请求。Redis提供了一种Pipeline(流水线)方法,可以提高上述用例的性能,我们来看看。从RedisPipeline交互模型可以看出,客户端先将执行的命令写入缓冲区(内存),最后一次性发送给Redis。管道将一批命令打包,然后发送给服务器。服务端执行后,将它们打包并按顺序返回,减少了频繁交互的时间,提高了性能。基本上使用Pipelinepipeline=jedis.pipelined();elementsfor(inti=0;i<1000;i++){pipeline.rpush("rediskey",i+"");}//执行pipeline.sync()使用起来还是很简单的。Pipeline的本质我们深入分析了请求交互的过程,真实情况是非常复杂的。上图是一个完整的请求交互流程图:客户端调用write将消息写入操作系统内核为socket分配的发送缓冲区。sendbuffer系统内核将buffer区的内容发送给网卡,网卡硬件通过路由将数据发送给服务器的网卡。服务器网卡将数据放入内核为套接字分配的接收缓冲区。将响应内容发送到发送缓冲区。服务器内核将缓冲区的内容通过路由发送给客户端的网卡。客户端内核将网卡中的数据放入接收缓冲区。客户端调用read从缓冲区中读取数据。总结一开始我以为写操作要等到对方收到消息后才会返回,其实不是这样的。写操作IO操作真正耗时的写操作只负责将数据写入本地操作系统内核的发送缓冲区,然后返回。剩下的就交给操作系统内核,把数据异步??发送到目标机器。但如果发送缓冲区已满,则需要等待缓冲区释放空闲空间,这才是写IO操作真正耗时的地方。读操作IO操作的实时消耗我们开始以为读操作是从目标机拉取数据,其实不然。读操作只负责从本地操作系统内核的接收缓冲区中取出数据。但是如果缓冲区为空,则需要等待数据的到来,这才是读操作的IO操作真正的耗时。value=redis.get(key)操作耗时对于value=redis.get(key)这样的简单请求,write操作几乎不耗时,写入发送缓冲区后直接返回,而read会耗时更多time因为它要等待响应消息通过网络路由到目标机器处理后,再送回当前内核读缓冲区才返回。管道的真正耗时对于管道来说,连续的写操作是一点都不耗时的,然后第一次读操作会等待一个网络往返开销,然后所有的响应消息都已经发回内核的读缓冲区。后续的读操作可以直接从缓冲区中获取结果并即时返回。优缺点Pipeline优点:pipeline通过打包命令执行一次性执行,可以节省连接->发送命令->返回结果过程产生的往返时间,减少I/O调用(从用户态到内核模式)切换)次。Pipeline的缺点:pipeline不能在每批中打包过多的命令,因为pipeline方式是将命令打包然后发送,那么redis在处理所有命令之前必须缓存所有命令的处理结果。这有内存消耗。管道不保证原子性。命令执行过程中,如果一条命令出现异常,其他命令会继续执行,所以如果需要原子性,不推荐使用流水线。管道一次只能作用于一个Redis节点(下面解释原因)适用场景一些系统可能对可靠性要求很高,每次操作都需要立即知道操作是否成功,数据是否已经写入redis,所以这个场景不适合。有些系统可能会批量向redis写入数据,允许一定比例的写入失败,那么可以使用这种场景,比如10000条记录一次进入redis,可能两条记录失败都无所谓,只要有后面的补偿机制比如你一次性发送10000条短信,按照第一种模式实现,客户端响应请求的时间会很长。这个延迟太长了。如果客户端请求设置了超时5秒,那么肯定会抛出异常,而且群发短信的实时性要求不是那么高,所以这时候最好使用pipeline。使用Pipeline的建议虽然好用,但是Pipeline每次组装的命令数量一定不能不受控制。否则一次组装的Pipeline数据量太大。一方面会增加客户端的等待时间,另一方面会造成一定的网络拥塞。一个包含大量命令的Pipeline被拆分成多个更小的Pipeline来完成Pipeline压力测试。Redis自带压力测试工具redis-benchmark,可以用来做流水线测试。Tips:redis-benchmark官方文档:https://redis.io/topics/benchmarks首先我们对一个普通的set命令进行压测,QPS约为5w/s。>redis-benchmark-tset-qSET:51975.05requestspersecond我们添加pipeline选项-P参数,表示单个pipeline的并行请求数,见下图P=2,QPS达到9w/s。>redis-benchmark-tset-P2-qSET:91240.88requestspersecond然后看P=3,QPS达到了10w/s。SET:102354.15requestspersecond其他问题为什么pipeline只能在一个Redis节点上工作,即pipeline不能用在集群模式下?我们知道Redis集群的key空间被划分为16384个槽,每个master节点负责处理16384个hash槽中的一部分。具体的redis命令会根据key计算出一个槽(slot),然后根据槽对具体的节点redis进行操作。如下图:master1(slave1):0~5460master2(slave2):5461~10922master3(slave3):10923~16383集群由三个master节点组成,其中master1分配0~5460的slot,master2是assignedslots为5461~10922slots,master3分配了slots从10923到16383。一个pipeline会批量执行多个command,所以每个command都需要根据key(CRC16.getSlot(key))计算出一个slot,然后根据slot在特定节点上执行命令,也就是说,一个管道操作会使用多个节点的redis连接,目前不支持。Tips:如果不了解Redis集群的知识,可以参考:https://redis.io/topics/cluster-tutorialpipeline和mget、mset等批量操作的区别?mget、mset也类似于pipeline,一次执行多条命令,一次发送出去,节省网络时间。对比如下:mset、mget操作是Redis队列中的原子操作,pipeline不是原子操作mset,mget操作的一个命令对应多个键值对,而pipeline是多个命令mset,mget是server实现,和pipeline的区别pipeline和事务是由server和client一起完成的?pipeline关注RTT时间,transaction关注一致性。Command+EXEC命令,所以至少请求2次),服务器顺序执行,返回一次。集群模式下,使用pipeline时,slotslot必须正确,否则服务器会返回redirectedtoslotxxx的错误;同时不建议使用事务,因为假设一个事务中的命令在MasterA上执行,也在MasterB上执行,A成功,B因故失败,所以数据不一致.这有点类似于分布式事务,不能保证绝对一致性。Pipeline对命令的数量有限制吗?没有限制,但是打包的命令不能太多,内存消耗会比较大。有多少命令适合Pipeline打包执行?查看官方Redis文档。根据官方文档的解释,建议每批次使用10k(注:此为参考值,请根据您的实际业务情况进行调整)。当pipeline被批量执行时,其他应用会不会无法读写?Redis采用多通道I/O复用模型和非阻塞IO,所以在管道批量写入时,在一定范围内不会影响其他读操作。本文转载自微信公众号“月亮与飞鱼”,您可以通过以下二维码关注。转载本文请联系月版飞语公众号。日常加油站
