本系列主要是为了对redis的网络模型和集群原理进行学习,我会用golang实现一个reactor网络模型,并实现对redis协议的解析。

系列源码已经上传github

https://github.com/HobbyBear/tinyredis/tree/chapter2

redis 网络模型

在介绍redis网络模型前,我们先来看看redis的一条命令执行涉及到哪些阶段。

从连接读取数据 => 协议解析 => 命令执行 =>  将结果返回给客户端

而平时我们说的redis的单线程指的则是 命令执行阶段是单线程的,而redis6.0以后协议解析和返回结果给客户端都可以是多线程去进行处理。

由此衍生出redis6.0的网络模型如下:

主线程通过一个epoll实例负责监听连接和等待数据的到达,并且执行解析好的命令。

举个例子,当主线程执行epoll wait时发现有100个socket可读,并且这100个socket都是已经完成连接建立的socket,然后主线程就会把这100个socket分给子线程去进行读取, 当子线程将这些socket的 字节数据解析成redis命令后,会再把这些命令交再给主线程去执行。主线程执行完以后又将这些命令执行结果交给子线程去进行发送。

整个过程可以看到实际的网络io的读取是交给多个线程去执行了,但是命令的执行还是在一个线程完成的。

再用流程图去表示整个过程将会更加清晰,

可以看见,整个过程中命令执行是在主线程去顺序执行的。而现在我们就是要用golang去实现这样一个模型。

思考用golang如何实现这样的网络模型

要实现主线程等待子线程池执行的效果,我们可以用golang的sync.WaitGroup 实现主协程等待子协程执行完毕的效果,子协程去进行协议的解析,为此,在前面代码的基础上,我封装了两个队列,一个用于读,一个用于写,主协程在需要读写时就将任务丢给队列执行,并等待队列执行完毕。

type Server struct {
Poll *poll
addr string
listener net.Listener
ConnMap sync.Map
readQueue *ioReadQueue
writeQueue *ioWriteQueue
}

队列的实现则是利用sync.WaitGroup实现的等待效果,

type ioReadQueue struct {
seq atomic.Int32
handle ioReadHandle
conns []chan *Conn
wg sync.WaitGroup
requests []*Request
}

handle则是进行协议解析的方法,conns 存放的则是待解析协议的连接,在队列创建时,会启用conns长度大小的协程对conns里的连接进行读取。

func newIoReadQueue(perChannelLen int, handle ioReadHandle, goNum int) *ioReadQueue {
res := &ioReadQueue{handle: handle, wg: sync.WaitGroup{}, requests: make([]*Request, 0)}
conns := make([]chan *Conn, goNum)
for i := range conns {
conns[i] = make(chan *Conn, perChannelLen)
connlist := conns[i]
go func() {
for conn := range connlist {
....
res.wg.Done()
}
}()
}
res.conns = conns
return res
}

这样便最简单的实现了一个固定大小的协程池,然后继续进行接下来的代码编写。

对协议进行抽象

最终读队列是要对redis协议进行解析,redis的协议是被称作resp协议,由于我的重点并不是放在协议解析上,所以我不打算对resp协议的解析进行具体的讲解。但是我们要做的是对协议层进行抽象,因为当你实现一个网络框架后,你应该让你的框架支持不止一种协议,比如它同时支持http,icmp等等。

源码部分实现了对resp协议和行协议的解析,篇幅有限,所以本文只关注关键逻辑和设计即可

思考抽象的一个协议需要具备哪些方法

由于网络中传输的都是字节,要转换成特定的协议,必然涉及到字节与协议体的相互转换,所以协议必然有编码和解码的方法。并且对协议的处理也应该有上层应用定义,网络框架只管对数据传输。

这样我便设计出了下面的接口

type ProtocolMsg interface {
Bytes() []byte
} type Protocol interface {
ReadConn(c *Conn) (ProtocolMsg, error)
OnExecCmd(msg ProtocolMsg) ProtocolMsg
}

ReadConn 用于在连接可读时从连接读取协议数据,这将由应用层自己定义解码操作,OnExecCmd 用于处理解析出的协议消息,OnExecCmd返回结果到时则会由网络框架层进行发送,协议消息编码为字节数据的方式则是ProtocolMsg的Bytes 方法中定义的,ProtocolMsg 会由应用层自己实现。

️️不过这里还有一点要做修改,ReadConn定义的是解码的操作,直接将网络框架内部的Conn传递给应用层进行读写不够安全,更安全的方式是只传递一个读缓冲区给应用层进行读写。这样的好处还有一个,因为我们的目的是非阻塞的读取连接里的数据,如果连接到达的数据还不能够组成一个完整的协议,那么这个连接已经读取出来的数据应该保存起来,供下次读取时,组成一个完整的协议。

所以,我们要为Conn这个结构定义一个缓冲区了。在通过redis学网络(1)-用go基于epoll实现最简单网络通信框架 里我们定义的Conn结构是

type Conn struct {
s *Server
conn *net.TCPConn
nfd int
}

现在还需要为它加一个缓冲区结构,并且为了复用缓冲区内存,我实现了一个RingBuffer 结构,关于其实现的讲解可以参考我的这篇文章go 实现ringbuffer以及ringbuffer使用场景介绍

目前Conn结构将变为

type Conn struct {
s *Server
conn *net.TCPConn
reader *RingBuffer
nfd int
}

然后协议解码接口我将把缓冲区传进去

type Protocol interface {
ReadConn(c *Conn) (ProtocolMsg, error)
OnExecCmd(msg ProtocolMsg) ProtocolMsg
}

环形缓冲区RingBuffer 的方法我再在这里列举一下


func (r *RingBuffer) Peek(readOffsetBack, n int) ([]byte, error) {
...
} func (r *RingBuffer) PeekBytes(readOffsetBack int, delim byte) ([]byte, error) { ...
} func (r *RingBuffer) AddReadPosition(n int) {
...
}

RingBuffer的peek开头的方法不会改变读指针的位置,但是会读取数据,AddReadPosition则是应用层在判断是完整协议后调用的方法,用于更新读指针的位置。

接着再来看服务器的handle方法主要做的改动。我在关键步骤标记了注释

func (s *Server) handler() {
for {
// 等待连接事件到达
events, err := s.Poll.WaitEvents()
if err != nil {
log.Error(err.Error())
continue
}
for _, e := range events {
connInf, ok := s.ConnMap.Load(int(e.FD))
if !ok {
continue
}
conn := connInf.(*Conn)
if IsClosedEvent(e.Type) {
conn.Close()
continue
}
// 连接可读,则放到读队列
if IsReadableEvent(e.Type) {
s.readQueue.Put(conn)
}
}
// 等待读队列任务完成
s.readQueue.Wait()
requests := s.readQueue.requests
replyMsgs := make([]*Request, 0)
// 将读队列解析好的命令进行执行
for _, request := range requests {
replyMsgs = append(replyMsgs, &Request{msg: s.protocol.OnExecCmd(request.msg), conn: request.conn})
}
// 将执行结果放回到写队列执行
for _, replyMsg := range replyMsgs {
s.writeQueue.Put(replyMsg)
}
// 等待些队列执行完毕
s.writeQueue.Wait()
// 由于读队列缓存了中间结果,一次轮询后进行清除
s.readQueue.Clear()
}
}

对于代码的讲解和设计思路,我这里只是列出了关键的点,细节可能还是需要去看下源码是如何实现的。但是最重要的是通过这个小demo,理解epoll的网络编程并且理解了redis的网络模型。

通过redis学网络(2)-redis网络模型的更多相关文章

  1. 网络分裂 redis 集群

    REDIS cluster-tutorial -- Redis中文资料站 -- Redis中国用户组(CRUG) http://www.redis.cn/topics/cluster-tutorial ...

  2. [转]网络协议-redis协议

    Redis 通信协议(protocol) 本文档翻译自: http://redis.io/topics/protocol . Redis 协议在以下三个目标之间进行折中: 易于实现 可以高效地被计算机 ...

  3. redis(二)Redis适用场景,如何正确的使用

    网络IO模型 Memcached是多线程,非阻塞IO复用的网络模型,分为监听主线程和worker子线程,监听线程监听网络连接,接受请求后,将连接描述字pipe 传递给worker线程,进行读写IO, ...

  4. Redis进阶实践之七Redis和Lua初步整合使用

    一.引言        Redis学了一段时间了,基本的东西都没问题了.从今天开始讲写一些redis和lua脚本的相关的东西,lua这个脚本是一个好东西,可以运行在任何平台上,也可以嵌入到大多数语言当 ...

  5. Redis进阶实践之七Redis和Lua初步整合使用(转载 7)

    Redis进阶实践之七Redis和Lua初步整合使用 一.引言 Redis学了一段时间了,基本的东西都没问题了.从今天开始讲写一些redis和lua脚本的相关的东西,lua这个脚本是一个好东西,可以运 ...

  6. Redis(Windows安装方法与Java调用实例 & 配置文件参数说明 & Java使用Redis所用Jar包 & Redis与Memcached区别 & redis-cli.exe命令及示例)

    Windows下Redis的安装使用 0.前言 因为是初次使用,所以是在windows下进行安装和使用,参考了几篇博客,下面整理一下 1.安装Redis 官方网站:http://redis.io/ 官 ...

  7. 3.用Redis Desktop Manager连接Redis

    Redis Desktop Manager是Redis图形化管理工具,方便管理人员更方便直观地管理Redis数据. 然而在使用Redis Desktop Manager之前,有几个要素需要注意: 一. ...

  8. 用Redis Desktop Manager连接Redis

    Redis Desktop Manager是Redis图形化管理工具,方便管理人员更方便直观地管理Redis数据. 然而在使用Redis Desktop Manager之前,有几个要素需要注意: 一. ...

  9. Redis实战之征服 Redis + Jedis + Spring (一)

    Redis + Jedis + Spring (一)—— 配置&常规操作(GET SET DEL)接着需要快速的调研下基于Spring框架下的Redis操作. 相关链接: Redis实战 Re ...

  10. [Redis] windows下安装 Redis

    一:Redis是什么? Redis是一个开源的使用ANSI C语言编写.支持网络.可基于内存亦可持久化的日志型.Key-Value数据库,并提供多种语言的API. 通过https://github.c ...

随机推荐

  1. 实现一个CRDT工具库——PSet

    PSet 这段代码实现了一个PSet,即Positive Set,是GSet的扩展.PSet是一个集合,支持添加和删除元素,但是不支持重复元素.PSet的实现是通过两个GSet来实现的,一个GSet存 ...

  2. [Linux]U盘启动盘安装CentOS7系统(LiveGNOME版)

    1 准备:工具 U盘 容量:视CentOS具体版本需要而定.推荐4G+ UltraISO(软碟通) ISO制作工具 / U盘启动盘刻录工具 2 制作:U盘启动盘 step1 下载CentOS镜像 Ce ...

  3. TypeScript必知三部曲(一)TypeScript编译方案以及IDE对TS的类型检查

    TypeScript代码的编译过程一直以来会给很多小伙伴造成困扰,typescript官方提供tsc对ts代码进行编译,babel也表示能够编译ts代码,它们二者的区别是什么?我们应该选择哪种方案?为 ...

  4. 重新理解RocketMQ Commit Log存储协议

    本文作者:李伟,社区里大家叫小伟,Apache RocketMQ Committer,RocketMQ Python客户端项目Owner ,Apache Doris Contributor,腾讯云Ro ...

  5. linux shell 自动化部署 npm vue 项目

    此 shell 是提供给前端登录服务器自动化部署 vue 项目的 用此命令,工具化部署项目,可以杜绝前端自己部署项目时,对服务器违规操作 如有其它问题,可在下方留言! #!/bin/sh # url: ...

  6. c++基本数据结构

    基本数据结构: 一.线性表 1.顺序结构 线性表可以用普通的一维数组存储. 你可以让线性表可以完成以下操作(代码实现很简单,这里不再赘述): 返回元素个数. 判断线性表是否为空. 得到位置为p的元素. ...

  7. c语言趣味编程(1)百钱百鸡

    一.问题描述 百钱买百鸡问题:公鸡五文钱一只,母鸡三文钱一只,小鸡三只一文钱,用100文钱买100只鸡,公鸡.母鸡.小鸡各买多少只 二.设计思路 (1)定义三个变量下x,y,z代表公鸡,母鸡,小鸡的数 ...

  8. uniapp directive 在原生 wgt 包不生效 uniapp directive 不生效

    需求 根据权限编码禁用按钮 阻止当前 dom 绑定的点击事件,禁用状态(opacity 半透明?? 或者 display: none?? ) 尝试 开发环境用 Chrome 跑,一切正常,构建打包后去 ...

  9. 如何生成文本: 通过 Transformers 用不同的解码方法生成文本

    简介 近年来,随着以 OpenAI GPT2 模型 为代表的基于数百万网页数据训练的大型 Transformer 语言模型的兴起,开放域语言生成领域吸引了越来越多的关注.开放域中的条件语言生成效果令人 ...

  10. 基于Java开发的全文检索、知识图谱、工作流审批机制的知识库

    一.项目介绍 一款全源码,可二开,可基于云部署.私有部署的企业级知识库云平台,应用在需要进行常用文档整理.分类.归集.检索的地方,适合知识密集型单位/历史文档丰富的单位,或者大型企业.集团. 为什么建 ...