Redis客户端在发送诸如get、set等命令时,服务端响应并发送回复,整个链路采用了request-reply网络处理模型。本文从源码角度主要分析服务端如何处理来自客户端的request:即服务端采取的事件处理机制、如何响应客户端的连接建立及读写请求。从C++, java和C语言版本的客户端源码展开阐述客户端如何接收和解析来自服务端的reply。
服务端事件处理总体架构
Redis服务端与客户端的⽹络IO通过事件驱动完成:连接建⽴、连接套接字读写封装成事件;由事件分发器管理这些事
件的添加、删除、更新和分发。常⻅的⾼性能事件分发器有 epoll, kqueue 等。服务端启动后,为⽹络IO事件的存储分
配空间、执⾏事件循环。每⼀轮循环中,往事件管理器添加事件或更新事件的状态;若有事件触发,则依次将被触发
的事件转移到事件发布队列内,并遍历该队列依次处理事件,执⾏⽹络 IO,事件驱动模型图如下图所示。
代码脉络
服务端⽹络IO事件处理机制
redis内部⾃⾏实现类似libuv中的event loop机制:⽂件ae.h和ae.c。⽀持多种IO多路复⽤器(epoll, kqueue,…);其
中,常⻅的⽹络IO事件以aeFileEvent类抽象,mask表示当前事件的类型、rfileProc和wfileProc是当读或写事件发⽣时,根
据事件类型执⾏相应的回调函数。
Redis服务端启动后,向事件管理器注册监听套接字事件。当有来⾃客户端的连接建立请求时,监听套接字事件对应的事件
处理函数acceptTcpHandler会建⽴连接套接字并调⽤createClient函数初始化客户端数据结构;并且为该客户端创建
可读事件,该事件的对应处理函数是conn->type->ae_handler。
在后续服务端同客户端的⽹络IO中,当某套接字上有读事件或写事件发⽣时,eventLoop中会去执⾏ae_handler也就
是下图中的connSocketEventHandler函数;该函数内部会进⼀步执⾏该套接字对应事件在加⼊IO多选器时注册的
read_handler和write_handler(该handler通过set_read/write_handler函数注册);⽐如执⾏read_handler:
readQueryFromClient、write_handler: replyToClient。readQueryFromClient主要读取服务端内核缓冲区的请求指令,并进行相应的哈希计算与查找;当内核缓冲区有空间可写入reply时,服务端会调用replyToClient函数将计算的结果回复到客户端。
客户端事件处理运⽤机制
客户端可采用同步通信的方式:发送命令,并等待服务端执行结果后的回复;亦可采用异步的通信方式,即客户端发送命令后,可执行其他操作,当服务端回复到客户端时,客户端响应获取该回复即可。
客户端hiredis的异步IO实现中,仍然使⽤事件驱动机制实现(此处仍然以redis内部基于ae.h的实现为例):通过
redisAeAddRead和redisAeAddWrite函数分别向IO多路分发器内发射客户端可读/可写事件;可以观察到当有读事件发
⽣时会调⽤redisAeReadEvent函数(写事件发⽣时同理);redisAeReadEvent函数内部调⽤的
redisAsyncHandleRead进⽽会调⽤hiredis.h中的bufferRead和getReply函数(调⽤同步IO⽅式下的hiredis.c内部实
现),也会调⽤其他回调函数。
使⽤hiredis库实现异步的Redis客户端时,可通过函数调⽤redisAsyncCommand,触发写事件后,将缓冲区的数据异
步发送⾄服务端;服务端响应命令后,触发客户端eventloop中事件处理器的读事件:客户端执⾏redisAsyncRead,
⼀⽅⾯读取输⼊缓冲区,另⼀⽅⾯重新往事件处理器中注册读事件,并执⾏应⽤层传⼊的回调函数
(redisProcessCallbacks(redisAsyncContext))。回调函数的函数原型是void (*callback)(const redisAsyncContext,
void *r, void *privatedata)。
Redis客户端通信实现
本节主要阐述不同语言版本下,异步Redis客户端的大致实现思路。此外,上文提到过,服务端通过replyToClient将reply通过网络回复到客户端。客户端接收的数据满足RESP3协议,需要将该网络数据反序列化成Redis对象(字符串、数组等),C++版本的redis-plus-plus客户端底层复用了hiredis的实现,Java Netty版本自行实现了该反序列化过程。因此,重点讲述Netty redis和hiredis数据反序列化的实现。
redis-plus-plus客户端
redis-plus-plus是c++实现的redis客户端库
在redis-plus-plus实现中,使⽤libuv实现事件驱动机制,进⽽实现客户端异步IO,利⽤hiredis库中的
redisLibuvAttatch建⽴redis-plus-plus同hiredis的关联。
redis-plus-plus中,可通过构造AsyncRedis启动异步的Redis客户端:在AsyncRedis的构造函数中,会创建uv_loop_t
对象,并创建⼀个⼦线程执⾏event_loop的逻辑。在AsyncRedis的析构函数中,由于主线程(调⽤析构函数的线程)
和eventloop线程共享某些变量,所以需要额外的同步机制保证线程安全。
在执⾏异步IO过程时,事件多路选择器调⽤IO事件对应的回调函数时是在区别于主线程的⼦线程中执⾏的(该⼦线
程也是libuv创建的eventLoop绑定的线程)。如下图,异步的set和get命令的处理及应⽤层传⼊的回调函数均会在⼦
线程中执⾏【set key1 val1(返回1) get key1(返回val1)】。
Netty Redis客户端
基于Netty的Redis客户端实现了异步的⽹络IO模型。
客户端与服务端连接建立以后,为客户端构造channelPipeLine,往该pipeline中加⼊ChannelHandler(实现InboundHandler);并以此构造Channel;Channel构造完成后,绑定到某⼀个Netty EventLoop中。
Netty-redis客户端接收到服务端发送的回复后,经由ByteToMessageDecoder将⽹络中的字节数据转成RedisMessage
类(转换的过程遵循RESP3协议);具体来讲,RiedsDecoder实现了ByteToMessageDecoder接⼝,内部采⽤状态机
⽅式将⽹络中的字节流转换成RedisMessage的各个⼦类的功能。以string类型和array类型的回复的字节流为例,
RedisDecoder会将字节流相应的转换成ArrayHeaderRedisMessage类和BulkStringHeaderRedisMessage类。
上述的Header类表明其后会尾随多个RedisMessage⼦类;多个RedisMessage可经由BulkStringAggregator类或
ArrayAggregator类聚合成⼀个RedisMessage⼦类。前者聚合成RedisMessage,后者聚合成⼀个
ArrayRedisMessage。
RedisBulkStringAggregator继承⾃MessageAggregator;后者和RedisArrayAggregator都继承⾃
MessageToMessageDecoder类(重载该类中的decode⽅法)。当服务端的reply数据包含嵌套层级结构时,RedisArrayAggregator类可通过深度优先搜索拆解该结构。其内部的成员depths⽤于存储数组类型嵌套情形下,递归解析时的上下⽂(AggregateState),depths当前的size表示⽬前数组嵌套的层数。
Hiredis客户端
hiredis中,也涉及从服务端获取的⽹络数据按照RESP3协议解析的过程(read.c⽂件中实现)。其中,处理数
组/Map/Set等聚合型数据的算法类似于Netty中的实现。
下图中,redisReader结构体内与redisReadTask遍历相关的属性定义包括tasks(相当于解析栈的总深度)。
buf属性存储了客户端从服务端读取的回复,len表示回复的⻓度;pos记录了当前处理数据的位置;ridx表明当前当前
所处的解析栈的深度,解析嵌套层次时,深度优先解析完当前任务后,再返回到上⼀层任务;当存在嵌套解析的情况
时,ridx递增。
下图中read.h⽂件定义的redisReadTask结构体相当于netty-redis中定义的AggregateState。每⼀层解析当前数组
时,数组的⻓度为elements,解析的当前数组的遍历下标为idx。
redisReaderTask结构中,idx表示当前构造的redis对象(string, integer, array ...)在数组中的位置,数组的元素(redis
对象)内容由obj赋予;当且仅当idx等于其⽗节点(相当于递归调⽤栈的上⼀层)的数组⼤⼩(elements属性)时,当前
数组解析结束;调⽤栈pop,返回上⼀层继续解析。这段逻辑在read.c⽂件中的moveToNextTask函数中。
总结
通过源码分析Redis服务端和异步客户端实现方式,可以看出基于事件驱动的网络请求处理模型的应用;理解Redis命令处理背后的原理。此外,源码中的一些实现方式,比如Netty中为反序列化RESP3协议数据所设计的类结构、深度优先解析嵌套层级结构数据的实现方法可作为参考用于日常或工作中的类似场景中。