引言
SpringBoot自2.0版本开始默认使用Lettuce作为Redis的客户端(注1)。Lettuce客户端基于Netty的NIO框架实现,对于大多数的Redis操作,只需要维持单一的连接即可高效支持业务端的并发请求——这点与Jedis的连接池模式有很大不同。同时,Lettuce支持的特性更加全面,且其性能表现并不逊于,甚至优于Jedis。本文通过分析Lettuce的特性和内部实现(基于6.0版本),及其与Jedis的对照比较,对这两种客户端,以及Redis服务端进行深度探讨。
NettyNIO框架概述
相信不少读者对Netty已经有一定的了解甚至使用。作为Lettuce的底层框架,本节我们首先对NettyNIO进行简单介绍。《NettyInAction》一书中提到:「从高层次的角度看,Netty致力于解决(网络编程领域)技术和体系结构两大我们关心的问题。首先,其构建于JavaNIO之上的异步及事件驱动的实现,保证了应用程序在高负载下的性能最大化和可伸缩性;其次,Netty运用一系列设计模式,将程序逻辑与网络层进行解耦,从而简化了用户的开发过程,并在最大程度上保证代码的可测性、模块化水平及可重用性。」
NettyNIO核心逻辑。图形绘制参考《NettyInAction》
上图展示了NettyNIO的核心逻辑。NIO通常被理解为non-blockingI/O的缩写,表示非阻塞I/O操作。图中Channel表示一个连接通道,用于承载连接管理及读写操作;EventLoop则是事件处理的核心抽象。一个EventLoop可以服务于多个Channel,但它只会与单一线程绑定。EventLoop中所有I/O事件和用户任务的处理都在该线程上进行;其中除了选择器Selector的事件监听动作外,对连接通道的读写操作均以非阻塞的方式进行——这是NIO与BIO(blockingI/O,即阻塞式I/O)的重要区别,也是NIO模式性能优异的原因。下文会结合Lettuce源码及性能分析,对Netty的设计模式及性能水平作进一步讨论。
Lettuce实现原理与Redis管道模式
Lettuceisascalablethread-safeRedisclientprovidingsynchronous,asynchronousandreactiveAPIs.MultiplethreadsmayshareoneconnectioniftheyavoidblockingandtransactionaloperationssuchasBLPOPandMULTI/EXEC.MultipleconnectionsareefficientlymanagedbytheexcellentnettyNIOframework.
上文摘自GitHub《LettuceWiki-AboutLettuce》中的介绍(注2),我们可以看到,虽然一个Netty的EventLoop可以服务于多个套接字连接,但是Lettuce仅凭单一的Redis连接即可支持业务端的大部分并发请求——即Lettuce是线程安全的。这有赖于以下几个因素的共同作用:
Netty的单个EventLoop仅与单一线程绑定,业务端的并发请求均会被放入EventLoop的任务队列中,最终被该线程顺序处理。同时,Lettuce自身也会维护一个队列,当其通过EventLoop向Redis发送指令时,成功发送的指令会被放入该队列;当收到服务端的响应时,Lettuce又会以FIFO的方式从队列的头部取出对应的指令,进行后续处理。Redis服务端本身也是基于NIO模型,使用单一线程处理客户端请求。虽然Redis能同时维持成百上千个客户端连接,但是在某一时刻,某个客户端连接的请求均是被顺序处理及响应的(注)。
Redis客户端与服务端通过TCP协议连接,而TCP协议本身会保证数据传输的顺序性。
Lettuce使用单一连接与Redis交互示意图
如此,Lettuce在保证请求处理顺序的基础上,天然地使用了管道模式(pipelining)与Redis交互——在多个业务线程并发请求的情况下,客户端不必等待服务端对当前请求的响应,即可在同一个连接上发出下一个请求。这在加速了Redis请求处理的同时,也高效地利用了TCP连接的全双工特性(full-duplex)。而与之相对的,在没有显式指定使用管道模式的情况下,Jedis只能在处理完某个Redis连接上当前请求的响应后,才能继续使用该连接发起下一个请求——Lettuce和Jedis之间的这种差异,在某种程度上与HTTP/2和HTTP/1之间的差异类似(注)。HTTP/2的实现原理读者可参阅《IntroductiontoHTTP/2》,本文不作赘述。
Redis