阅读Redis源码(三) — redis通信协议与事件驱动

在redis中,关于事件驱动框架的代码集中在ae.h/ae.c中.作者也在头部设置了介绍:a simple event-driven programming library. 这个框架其实很简单,核心就是一个消息
队列,同时只有一个线程负责对其进行处理,这里面的调度思想,还是简单的优先级队列,文件操作优先级永远高于时间操作.而且任务之间并不会进行抢占.

具体执行过程,可以参照如下干特图:

time ----------------------------------------------------------------------->|
     |<---- 10 ms ---->|<---- 10 ms ---->|<---- 10 ms ---->|<---- 10 ms ---->|
     | FE 1         | FE 2     | sC 1 | FE 3 | FE 4 |   FE 5  |    sC 2  |
     |<-------- 15 ms -------->|      |<------- 12 ms ------->|
            >= 10 ms                          >= 10 ms
     ^                         ^      ^                       ^
     |                         |      |                       |
  file event              time event  |                  time event
  handler                 handler     |                  handler
  run                     run         |                  run
                                 file event
                                 handler
                                 run

而redis对事件的处理逻辑可以简化为一下逻辑(对应的代码位于ae.c的aeProcessEvents()方法):

def process_event():
    # 获取执行时间最接近现在的一个时间事件
    te = get_nearest_time_event(server.time_event_linked_list)
    # 检查该事件的执行时间和现在时间之差
    # 如果值 <= 0 ,那么说明至少有一个时间事件已到达
    # 如果值 > 0 ,那么说明目前没有任何时间事件到达
    nearest_te_remaind_ms = te.when - now_in_ms()
    if nearest_te_remaind_ms <= 0:
        # 如果有时间事件已经到达
        # 那么调用不阻塞的文件事件等待函数
        poll(timeout=None)
    else:
        # 如果时间事件还没到达
        # 那么阻塞的最大时间不超过 te 的到达时间
        poll(timeout=nearest_te_remaind_ms)
    # 处理已就绪文件事件
    process_file_events()
    # 处理已到达时间事件
    process_time_event()

至此,事件部分结束.

然后就是通信协议了.Redis使用的是非阻塞的套接字,所以才可以在使用单线程的情况下处理多个客户机请求.

而服务端与客户端通信的协议也足够简单.比如,在使用./redis-cli通信时,当你输入get mike时,传输的数据就是$3
mike
,最先的3表明只有一行数据,
所以,在官方推荐的java驱动jedis中,jedis做的,就只是让用户从原始的手动拼接字符串变成了使用接口方法进行拼接,同时,将一些Exception进行了封装.仅此
而已.

以下是执行“set mike mike”的执行内容:

  • 假设现在C1连接到服务器S,执行命令 SET mike mike 时,客户端会拼接字符串成:”*3
    $3
    SET
    $4
    mike
    $4
    mike
    “,同时写入连接到服务器的套接字中。
  • 当S的文件事件处理器执行时,它会察觉到 C1 所对应的读事件已就绪,于是它将协议文本读入,并查询缓存。
  • 通过对查询缓存进行分析(parse), 服务器在命令表中查找 SET 字符串所对应的命令实现函数, 最终定位到 t_string.c/setCommand 函数, 另外, 两个命令参数 mike 和 mike 也会以字符串的形式保存在客户端结构中。
  • 接着, 程序将客户端、要执行的命令、命令参数等送入命令执行器: 执行器调用 setCommand 函数, 将数据库中 mike 键的值修改为 mike , 然后将命令的执行结果保存在客户端的回复缓存中, 并为客户端 fd 关联写事件, 用于将结果回写给客户端。
  • 因为mike键的修改, 其他和数据库命名空间相关程序, 比如AOF 、REPLICATION 还有事务安全性检查(是否修改了被 WATCH 监视的键?)也会被触发, 当这些后续程序也执行完毕之后, 命令执行器退出, 服务器其他程序(比如时间事件处理器)继续运行。
  • 当C1对应的写事件就绪时, 程序就会将保存在客户端结构回复缓存中的数据回写给客户端, 当客户端接收到数据之后, 它就将结果打印出来。

因为足够简单,所以,我们可以按照他的协议进行编写任意的driver.

同时在内网上看到了redis-proxy这个项目,其设定为,PHP客户端会频繁创建redis连接,进行获取数据,同时,因为php中不存在长连接(目前我的理解),所以大量的
创建和释放连接是一个比较浪费资源的行为,所以使用了netty作为了一个中间件,通过java保持对redis的长连接,对连接池化,同时在连接php端,通过继承netty的
高性能,提高反应速度.

因为在文档里并没有说明该proxy的使用场景,我的理解应该是放置在每一个php服务器上,如果只是放置在redis服务器上,这个所谓的proxy其实并没有多少意思,
最大的优化的地方应该就是将一个网络调用变成了一个本地回环.提高了响应速度.

结合蘑菇街的部署方案.可以看到,为了应对大量的短而集中的请求,使用了nginx进行了处理.
因为双方都是单线程模型,所以最大的瓶颈就在于传输大数据的时候,会阻塞.同时无法体现多核CPU的优势.后者可以通过起多个实例来体现多核优势(蘑菇街目前的在请求
缓存资源时使用的实例名称,感觉就是在指定某一个redis实例),对于前者,则需要在编码时注意缓存的内容不宜过大.如果需要则可以通过字符串压缩的方式来缩短字符的长度.

至此结束.

3 Replies to “阅读Redis源码(三) — redis通信协议与事件驱动”

Leave a Reply

Your email address will not be published. Required fields are marked *