掌秋使 手游攻略 手游评测 Redis客户端Lettuce深度剖析介绍(一)

Redis客户端Lettuce深度剖析介绍(一)

时间:2024-10-06 14:41:51 来源:其他 浏览:0

引言

Netty NIO框架概述

相信很多读者已经对Netty有了一定的了解甚至使用了。作为Lettuce的底层框架,本节我们首先简单介绍一下Netty NIO。《Netty In Action》书中提到:“从高层的角度来看,Netty致力于解决我们关心的(网络编程领域)技术和架构的两大问题。第一,它是建立在Java NIO异步和事件之上的。驱动的实现保证了应用程序在高负载下的最大性能和可扩展性;其次,Netty使用了一系列设计模式将程序逻辑与网络层解耦,从而简化了用户的开发过程并保证了可测试性、模块化和可重用性;最大程度地保留代码。”

上图展示了Netty NIO的核心逻辑。 NIO通常被理解为non-blocking I/O的缩写,意思是非阻塞I/O操作。图中Channel表示连接通道,用于承载连接管理和读写操作; EventLoop是事件处理的核心抽象。一个EventLoop可以服务多个Channel,但它只能绑定到一个线程。 EventLoop中的所有I/O事件和用户任务都在此线程上处理;除了Selector的事件监听动作外,对连接通道的读写操作都是以非阻塞的方式进行的—— 这是NIO和BIO(blocking I/O,即阻塞I/O)的一个重要差异也是NIO模式性能优异的原因。下面将根据Lettuce源码和性能分析进一步讨论Netty的设计模式和性能水平。

Lettuce实现原理与Redis管道模式

Lettuce 是一个可扩展的线程安全Redis 客户端,提供同步、异步和反应式API。如果多个线程避免阻塞和事务性操作(例如BLPOP 和MULTI/EXEC),则它们可以共享一个连接。优秀的netty NIO框架可以有效地管理多个连接。

以上摘自GitHub 《Lettuce Wiki - About Lettuce》(注2)中的介绍。我们可以看到,虽然一个Netty EventLoop可以服务多个socket连接,但Lettuce只需要单个Redis连接就可以支持大部分业务端。并发请求数—— 即Lettuce是线程安全的。这取决于以下因素的共同作用:

Netty的单个EventLoop仅绑定到单个线程。业务端的并发请求都会被放入EventLoop的任务队列中,最终由线程顺序处理。同时Lettuce本身也会维护一个队列。当它通过EventLoop向Redis发送指令时,发送成功的指令将被放入队列中;当Lettuce收到服务器的响应时,会以先进先出的方式从队列的头部开始。检索相应的指令以进行后续处理。 Redis服务器本身也是基于NIO模型,使用单线程处理客户端请求。虽然Redis可以同时维护成百上千个客户端连接,但在某一时刻,某个客户端连接的请求是按顺序处理和响应的(注3)。 Redis客户端和服务器通过TCP协议连接,TCP协议本身保证数据传输的顺序。

管道模式在Redis官网文档中有详细讨论。总体思路是(注5):客户端和服务器通过网络连接。无论两者之间的网络延迟高还是低,数据包从客户端发送到服务器(请求的过程)再从服务器返回客户端(响应)总是需要一定的时间。我们称这段时间为RTT(往返时间)。假设在高时延网络条件下,RTT达到250ms。此时,即使服务器有每秒处理100k个请求的能力,整体QPS(基于单个连接)也只有4。借助管道模式,客户端可以发出大量(如1k)的请求,然后一次性接收服务器的大量响应,从而显着提高请求处理速度。如下图:

作者使用JMH框架模拟客户端高并发请求(200个线程)的场景,并基于单个Redis连接测试上述功能。 Redis服务器运行在阿里云的ECS上,到客户端所在机器的RTT约为12ms。非管道模式下,比如基于Jedis客户端的单连接,理论上整体QPS只能达到80+,实测更低;而且也是基于单连接,在Lettuce客户端的帮助下,笔者测得7000+的QPS。当然,这里的对比主要是为了说明Lettuce的管道特性,因为Jedis在生产环境中总是和连接池结合使用。下面的文章将进一步对比和分析两个客户端正常使用时的性能。

注意,这里我们使用多个线程基于单个Redis连接执行并发请求,以测试Lettuce客户端的管道特性(其中每个线程本身发送请求并在请求结束之前接收响应),尽管这与我们一般理解的。基于单个Redis连接的单个线程一次发出多个请求的流水线方式存在一些差异,但两者本质上是相同的:即客户端可以在同一个Redis连接上发出后续请求,而无需等待对于前一个请求的响应。要求。对此的详细讨论可以在文章《Lettuce Wiki - Pipelining and command flushing》 中找到。现摘录部分内容如下,供读者参考。下面我们就根据Lettuce源码对这个特性进行详细的分析。

Lettuce 被设计为以管道方式运行。多个线程可以共享一个连接。虽然一个线程可以处理一个命令,但另一个线程可以发送一个新命令.Lettuce 构建在netty 之上,将读取与写入分离并提供线程安全连接。结果是,读取和写入可以由不同的线程处理,并且命令的写入和读取彼此独立但按顺序进行。

Lettuce核心源码分析

由于Lettuce是基于Netty框架实现的,所以它在初始化Redis连接通道(Channel)时,会向通道管道(ChannelPipeline)添加一系列需要使用的通道处理程序(ChannelHandler)来实现Redis连接管理。以及读写操作的处理。大致如下图所示:

上图中实线箭头的流程代表每个入站ChannelHandler对Redis的连接管理和读操作处理链路,而虚线箭头的流程代表每个出站ChannelHandler对Redis的写操作的处理链路。例如,在初始化Redis连接时,RedisHandshakeHandler默认会首先尝试使用RESP3协议与Redis进行交互;当发送Redis命令时,CommandEncoder作为出站链路上的最后一个处理器,会将Lettuce命令模型实例(RedisCommand)转换为最终可以写入Channel的字节容器(ByteBuf)。

我们在上一节第1 点中描述的Lettuce 对指令的操作行为是通过CommandHandler 处理器实现的。 CommandHandler继承了Netty预定义的ChannelDuplexHandler,并相应处理Redis的读写操作。可以说是一个核心通道处理器。下面我们进行详细的源码分析。

CommandHandler写操作

//CommandHandler编写操作相关核心代码,省略部分public class CommandHandler extends ChannelDuplexHandler Implements HasQueuedCommands { private final ArrayDequeRedisCommand?堆栈=新的ArrayDeque(); //重写ChannelDuplexHandler 的write 方法@Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise Promise) throws Exception { //如果msg 是单个Redis 命令if (msg instanceof RedisCommand) { writeSingleCommand(ctx, (RedisCommand?)消息,承诺);返回; } } private void writeSingleCommand(ChannelHandlerContext ctx, RedisCommand? command, ChannelPromise Promise) { //使用Promise 注册事件监听器addToStack(command, Promise); //将命令传递给下一个ChannelHandler(这里是CommandEncoder)handle ctx.write(command,promise); } private void addToStack(RedisCommand? command, ChannelPromise Promise) { 尝试{ RedisCommand? redisCommand=潜在WrapLatencyCommand(命令); if (promise.isVoid()) { stack.add(redisCommand); } else { //正常情况下,分支流程会走到这里,用promise注册监听器promise.addListener(AddToStack.newInstance(stack, redisCommand)); } } catch (Exception e) { command.completeExceptionally(e);扔e; } } //事件监听器static class AddToStack Implements GenericFutureListenerFutureVoid { private ArrayDequeObject stack;私有RedisCommand?命令; static AddToStack newInstance(ArrayDeque? stack, RedisCommand? command) { AddToStack 条目=RECYCLER.get(); Entry.stack=(ArrayDequeObject) 堆栈;条目.命令=命令;返回条目; } //当Redis命令成功写入socket缓冲区时,该方法会回调@Override public void operationComplete(FutureVoid future) { try { if (future.isSuccess()) { //将Redis命令添加到最后队列的stack.add(command); } } 最后{ 回收(); } } } }

以上就是CommandHandler写操作相关的核心代码(稍作简化)。可以看到,CommandHandler重写了ChannelDuplexHandler的write方法。当Lettuce将Redis请求传递到Netty EventLoop并执行时,会调用该方法。在发送单个Redis命令(RedisCommand)的场景下,接下来会调用CommandHandler#writeSingleCommand方法。

该方法主要负责完成两件事:第一,向通道操作结果Promise 注册一个事件监听器(AddToStack);第二,将指令传递给下一个ChannelHandler(这里是CommandEncoder)进行处理。当Redis命令最终通过JDK的SocketChannel成功写入socket缓冲区时,监听器的AddToStack实例的operationComplete方法将被回调并执行,命令将被放入CommandHandler实例维护的堆栈队列中。这样业务线程的并发请求就会被EventLoop依次处理,并按顺序放入指令队列。我们用时序图来更直观地展示组件之间的交互:

上图中,除了灰色部分的commandHandler、addToStack和commandEncoder这三个参与者是Lettuce的组件实例外,其余都是Netty或JDK的组件实例(图片稍微简化了)。由此,我们也可以看出Netty框架具有良好的扩展性和完善的预定义功能。从设计模式的角度来看,连接各个请求读写操作处理器的通道管道(ChannelPipeline)属于责任链模式;而AddToStack监听器的注册和回调则是采用了观察者模式——,两者都使得Netty框架更加符合开闭原则,并且易于扩展和使用。

CommandHandler读操作

同步调用场景下,业务线程通过Lettuce发送Redis命令后,会得到一个RedisFuture对象,并在该对象上等待获取请求的处理结果。将Redis响应放入RedisFuture实例的操作也是由CommandHandler完成的。前面我们介绍了CommandHandler对请求写操作的处理。这里我们分析处理器对响应读操作的处理。除了上面提到的ChannelDuplexHandler的write方法之外,CommandHandler还重写了其父类的channelRead方法。当EventLoop从Redis读取响应时,该方法将被调用。具体代码如下(稍作简化):

Redis客户端Lettuce深度剖析介绍(一)

//CommandHandler读操作相关的核心代码,部分省略public class CommandHandler extends ChannelDuplexHandler Implements HasQueuedCommands { private final ArrayDequeRedisCommand?堆栈=新的ArrayDeque();私有ByteBuf 缓冲区; //重写ChannelDuplexHandler父类ChannelInboundHandlerAdapter的channelRead方法@Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { ByteBuf input=(ByteBuf) msg; try { //将输入数据写入缓冲区buffer.writeBytes(input); //数据解析decode(ctx, buffer ); } 最后{ input.release(); } } //响应解析protected void decode(ChannelHandlerContext ctx, ByteBuf buffer) throws InterruptedException { while (canDecode(buffer)) { if (isPushDecode(buffer)) { //Sub 模式下Pub/Redis 消息处理,代码省略} else { //常规Redis 响应处理//以FIFO 方式从队列头部读取(但不取出)Redis 命令RedisCommand?命令=stack.peek( ); try { //响应解析if (!decode(ctx, buffer, command)) {decodeBufferPolicy.afterPartialDecode(buffer);返回; } } catch (Exception e) { //省略异常处理代码} if (isProtectedMode(command )) { //省略} else { if (canComplete(command)) { //取出Redis命令stack.poll(); try { //完成命令complete(command); } catch (Exception e) { //异常处理代码省略} } } //缓冲区数据清理afterDecode(ctx, command); } } } }

可以看到,在channelRead方法中,EventLoop读取到的输入数据首先会被写入到CommandHandler实例本身维护的缓冲区中,然后decode方法会进行具体的解析工作。这是因为一个socket读操作可能并不总是读到完整的Redis响应,所以我们需要先暂时保存数据。在响应解析过程中,CommandHandler处理器会以先进先出的方式读取写入操作时放入堆栈队列的Redis命令(RedisCommand)。由于前面提到的Lettuce、Redis和TCP连接在命令处理和传输中的顺序保证,我们可以保证当前响应数据属于正在读取的命令。如果解析成功,Redis响应数据将写入命令实例,命令最终会从队列中取出并标记为已完成;否则,该方法将直接返回,后续的channelRead调用将基于完整的响应。数据被解析。

值得注意的是,decode方法中的解析动作是在一个while循环中执行的。这是因为,在管道模式下,除了无法一次读取完整的Redis响应之外,一次socket读操作还可能读取到多个Redis响应。同步调用模式下,当Redis命令被标记为完成时,等待响应的业务线程就可以获得结果数据。如果调用方式为异步,则默认由EventLoop线程对响应结果进行后续处理。这里的RedisCommand指令对象看起来和前面提到的RedisFuture结果对象不同,但实际上它们都指向同一个实例(AsyncCommand)。至此,我们已经大致解释了Lettuce的核心读写逻辑。正是基于这样的设计,Lettuce只需要一个连接就可以服务于业务线程的并发请求,并且可以通过高效的管道模式与Redis进行交互。

Lettuce与Jedis的性能比较

从Redis服务端的角度比较分析

上图展示了Jedis和Redis之间的交互。乍一看,在业务线程高并发请求的场景下,Lettuce和Jedis运行模式的性能似乎不太容易区分。 —— 前者在单个共享连接上以管道模式与Redis 交互。后者通过其维护的连接池对Redis进行并发操作。我们先从Redis服务器的角度来分析一下。从Redis服务器的角度来看,当客户端请求发送速率相同时,管道交互方式具有一定的优势。这里引用Redis官网文档《Using pipelining to speedup Redis queries》:

流水线不仅仅是减少与往返时间相关的延迟成本的一种方法,它实际上极大地提高了

roves the number of operations you can perform per second in a given Redis server. This is the result of the fact that, without using pipelining, serving each command is very cheap from the point of view of accessing the data structures and producing the reply, but it is very costly from the point of view of doing the socket I/O. This involves calling the read() and write() syscall, that means going from user land to kernel land. The context switch is a huge speed penalty. 上文大意是:管道模式的作用不仅仅在于其减少了网络RTT带来的延迟影响,同时,它也显著提升了Redis服务器每秒可执行的指令操作量。这是因为,虽然从访问内存数据并生成响应的角度看,Redis处理某条指令操作的成本是很低的,但是从执行套接字I/O操作的角度看,如果我们不使用管道模式,(当需要逐个处理大量客户端请求时)对Redis来说(相对于内存操作)成本是很高的。套接字I/O操作涉及read和write这两个系统调用,这意味着Redis需要(频繁地)从用户态切换到内核态,而由此导致的上下文切换会非常耗时。 根据《深入理解计算机系统》中的介绍,上下文切换(context switch)发生在内核对系统中不同进程或线程的调度(scheduling)过程中。就进程而言,内核会为每个进程维护一个上下文(context),用于在需要的时候将被中断的进程恢复执行。进程上下文包括多种不同的对象,如各类寄存器、程序计数器、用户栈、内核栈,以及各类内核数据结构(如地址空间页表、文件表)等。当程序在用户态执行系统调用(如前面提到的套接字I/O操作)时,为了避免阻塞,内核会通过上下文切换机制,中断当前进程,并调度执行一个其他的(先前被中断的)进程。 这个过程包括:1、保存当前进程的上下文,2、恢复那个先前被中断的进程的被保存的上下文,3、执行这个被恢复的进程。此外,即使系统调用没有阻塞,内核也可以选择执行上下文切换,而不是(在系统调用完成后)将控制返回给调用进程。 可以看到,进程的上下文切换操作是较为复杂的。而对于运行在同一个进程中的线程来说,由于它们共享该进程的上下文,且线程自身的上下文比进程的上下文小不少,因此(同一个进程中的)线程的上下文切换相比进程的上下文切换要快。然而即便如此,由于同样涉及到程序在用户态和内核态之间的来回转换,以及CPU数据的刷写,高强度的线程上下文切换带来的性能损耗也是不可忽视的(注6)。这也是为什么很多框架和编程语言会尽可能避免这一点。比如Netty的EventLoop遵循Java NIO的模式,仅与单个线程绑定(注7);JDK 6中默认开启自旋锁,以尽可能减少线程切换的开销(注8);Go语言更是使用goroutine替代线程,以提高程序并发性能(注9)。

用户评论

丢了爱情i

这篇文章深入浅出地介绍了 Redis 客户端 Lettece 的特点和使用方法,特别适合初学者学习。

    有11位网友表示赞同!

黑夜漫长

Lettuce 提供了一个优雅的、易于使用的 API,使得操作 Redis服务器时更顺手。

    有8位网友表示赞同!

雁過藍天

我觉得 Lettece 客户端相比其他工具,更加轻便高效,非常适合作为项目中的备份选项。

    有20位网友表示赞同!

别悲哀

通过阅读这篇分析,我了解到了 Lettece 在处理大数据和高并发场景下的优势。

    有20位网友表示赞同!

暖栀

Lettuce 的文档清晰详尽,对我的开发有很大帮助,特别是快速原型搭建阶段。

    有17位网友表示赞同!

无望的后半生

在进行分布式系统设计时,使用 Lettece 作为 Redis 客户端可以提升整体的可靠性和可维护性。

    有7位网友表示赞同!

遗憾最汹涌

文章中提到的 Lettece 非阻塞 API 能让程序响应更快、效率更高。

    有13位网友表示赞同!

身影

Lettuce 的连接池管理功能使得在处理大量 Redis 连接时,代码变得更加简洁和高效。

    有18位网友表示赞同!

旧事酒浓

对于使用 Java 开发的应用来说,Lettuce 是一个非常棒的集成解决方案,减少了开发中的额外学习成本。

    有8位网友表示赞同!

封心锁爱

我特别喜欢 Lettece 对并发操作的支持,能够轻松地对多 Redis 实例进行无缝管理。

    有9位网友表示赞同!

打个酱油卖个萌

在实际项目中,我发现 Lettece 的性能调优指南部分对于保证服务稳定运行有很大的帮助。

    有7位网友表示赞同!

安好如初

这篇文章不仅讲了理论,还分享了一些实用技巧,比如如何优化 Lettece 和 Redis 服务器之间的交互速度。

    有8位网友表示赞同!

■孤独像过不去的桥≈

对于热衷于提升 Redis 应用程序响应效率的开发者来说,Lettuce 提供了一系列值得学习的最佳实践指南。

    有20位网友表示赞同!

眷恋

Lettuce 的文档示例很丰富,能够快速帮助开发者上手并进行定制化开发。

    有20位网友表示赞同!

枫无痕

我在 Lettece 的社区里也获得了一些有用的反馈和建议,这对我的项目改进真的非常有帮助。

    有14位网友表示赞同!

有你,很幸福

这篇文章中的故障排查部分对我在维护 Redis 客户端逻辑时遇到的问题提供了很好的解决方案。

    有9位网友表示赞同!

铁树不曾开花

Lettuce 在处理异步请求中的表现让我对它的性能更加有信心,尤其是在高负载环境下的稳定性。

    有8位网友表示赞同!

杰克

对于热衷于探索开源项目的开发者,Lettuce 的源代码注释详细且清晰,值得一探究竟。

    有11位网友表示赞同!

标题:Redis客户端Lettuce深度剖析介绍(一)
链接:https://www.zhangqiushi.com/news/sypc/15139.html
版权:文章转载自网络,如有侵权,请联系删除!
资讯推荐
更多
绯红之境兑换码最新2021 礼包兑换码大全

绯红之境兑换码最新2021 礼包兑换码大全[多图],绯红之境兑换码怎么领取?绯红之境兑换码有哪些?绯红之境在今日

2024-10-06
妄想山海怎么加好友 加好友方法大全

妄想山海怎么加好友 加好友方法大全[多图],妄想山海添加好友功能在哪里?妄想山海添加好友的方法是什么?好友添

2024-10-06
三国群英传7霸王再临攻略 霸王再临攻略技巧开启方法

三国群英传7霸王再临攻略 霸王再临攻略技巧开启方法[多图],三国群英传7霸王再临怎么玩?三国群英传7霸王再临

2024-10-06
江南百景图又见桃花村钓鱼位置在哪?又见桃花村钓鱼攻略

江南百景图又见桃花村钓鱼位置在哪?又见桃花村钓鱼攻略[多图],江南百景图又见桃花村钓鱼怎么钓?又见桃花村钓

2024-10-06