本文主要是介绍来聊聊我用go手写redis这件事,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
写在文章开头
网上有看过一些实现redis
的项目,要么完全脱离go语言
的理念,要么又完全去迎合c的实现理念,也不是说这些项目写的不好,只能说不符合笔者所认为的那种"平衡"
,于是整理了一段时间的设计稿,自己尝试着用go语言
写了一版"有redis味道"
的mini-redis
。
截至目前,笔者已经完成了redis
服务端和客户端交互的基本通信架构和实现基调,如下所示,可以看到笔者已经实现了ping
和command
指令:
127.0.0.1:6379> ping
PONG
127.0.0.1:6379> command
1) "COMMAND"
2) "PING"
后续也会一直沿用命令模式
的基调不断完善指令即可,而这篇文章算是笔者的一篇引子,也算是从笔者的设计稿中整理出来的一篇个人实现的思路的复盘,希望这个系列对C
、Go
、Java
开发能有一些灵感。
Hi,我是 sharkChili ,是个不断在硬核技术上作死的 java coder ,是 CSDN的博客专家 ,也是开源项目 Java Guide 的维护者之一,熟悉 Java 也会一点 Go ,偶尔也会在 C源码 边缘徘徊。写过很多有意思的技术博客,也还在研究并输出技术的路上,希望我的文章对你有帮助,非常欢迎你关注我的公众号: 写代码的SharkChili 。
因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 “加群” 即可和笔者和笔者的朋友们进行深入交流。
用go语言实现redis整体通信架构
mini-redis的整体通信基调
我们都知道go语言
是goroutine-per-connection
的语言,相较于C、Java那种多线程的语言来说,协程切换的开销远远小于另外两门多线程语言,所以笔者的设计之初的就认定实现的思路就是起一个协程监听6379
连接,每当收到一个客户端请求,就为其分配一个协程监听起客户端的读写请求:
服务端/客户端结构体抽象
C语言
所实现的redis服务端是通过结构体redisServer进行抽象,在设计之初笔者也沿用这个结构体,为了把握核心脉络笔者只用到了如下几个字段:
ip
:记录redis的ip地址port
:记录redis服务端的端口号clients
:记录每一个与redis
服务端建立的客户端连接信息,原生redis
因为通过单线程epoll
处理连接和读写事件,所以维护客户端的数据结构采用链表。而笔者所实现的mini-redis
涉及多协程,所以为了保证并发安全采用sync.Map
,通过客户端信息序列化作为key
,客户端指针作为value
进行管理。
C语言
通过redisClient
结构体记录与服务端建立连接的客户端文件描述符fd
等信息,而go语言
对于这些传输层的概念都做了封装,我们只能拿到封装好的连接对象net.Conn
,所以目前为止,笔者的的客户端之抽象了连接字段。
实现中的注意事项
了解整体的结构设计,接下来就是对整个系统的交互边界的设计了,包括:
- 程序关闭,如何优雅关闭监听和客户端连接。
- 关闭期间收到客户端连接怎么办?
- 处理连接遇到异常怎么办?
先来回答第一个问题,按照go语言
的设计理念,我们可以通过channel
向其他协程传递程序关闭的信号,一旦监听关闭信号的协程收到连接关闭的信号之后,先将监听关闭,然后遍历clients
的集合将所有客户端连接关闭:
来到问题2,在监听关闭的一瞬间,正准备处理的连接如何优雅关闭呢?同样的我们通过一个原子变量done
来管理,一旦收到新连接准备交由acceptTcpHandler
处理时,我们可以查看done的值是不是1,如果是则直接将该连接关闭:
针对问题3,笔者的处理方式相对粗暴一些,监听连接遇到错误时,一律判定为服务器准备关闭,直接将监听的socket
关闭掉即可。
整体通信基调的实现
经过上述的设计与梳理,想必大家对笔者的设计思路有了大致的了解,首先我们先定义一下redisServer
的结构体,代码如下所示,可以看到除了必要的记录ip和端口号的字段以外,还有用于监听关闭服务的channel包括shutDownCh
和原子变量done
,对于新连接的客户端统一用 clients
进行管理:
type redisServer struct {//record the ip and port number of the redis server.ip stringport int//semaphore used to notify shutdown.shutDownCh chan struct{}closeClientCh chan redisClientdone atomic.Int32//record all connected clients.clients sync.Map//listen and process new connections.listen net.Listener
}
对应的客户端连接信息如下,非常简单,结构体中只包含了记录redis
客户端连接的变量conn
:
type redisClient struct {//redis client connection infoconn net.Conn
}
随后我们就可以给出主脉络,和原生redis一样,我们也用go语言全局声明一个结构体redisServer
:
var server redisServer
然后进行初始化配置:
//initialize redis server and configurationinitServer()loadServerConfig()initServerConfig()
这里面大部分逻辑笔者没有进行细化拓展,最核心的逻辑就是在initServer中初始化redis服务端结构体中各种成员变量的信息如ip地址、端口号、channel等信道初始化:
func initServer() {log.Println("init redis server")server.ip = "localhost"server.port = 6379server.shutDownCh = make(chan struct{})server.closeClientCh = make(chan redisClient)createSharedObjects()
}
随后我们通过go语言
的signal
监听进场关闭的信号,声明一个闭包函数传入server的指针,一旦该协程收到channel
的信号之后则则直接将redis
监听和客户端的连接全部关闭:
sigCh := make(chan os.Signal)signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT)go func(server *redisServer) {sig := <-sigChswitch sig {case syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT:closeRedisServer()//modifying atomic variables means that the server is ready to shut down.server.done.Store(1)}}(&server)
然后就是基于配置生成地址进行绑定生成的listen
指针直接交给server
的listen
:
//parse address informationaddress := server.ip + ":" + strconv.Itoa(server.port)log.Println("this redis server address:", address)//binding port numberlisten, err := net.Listen("tcp", address)if err != nil {log.Fatal("redis server listen failed,err:", err)return}server.listen = listen
后续我们就专门使用一个协程监听新连接然后交给acceptTcpHandler
函数处理(函数名源于redis的处理连接的方法,可以说笔者基本沿用的redis
的核心理念和实现):
//listen for incoming connections.go func() {for {log.Println("event loop is listening and waiting for client connection.")conn, err := listen.Accept()if err != nil {log.Println("accept conn failed,err:", err)break}acceptTcpHandler(conn)}}()
而acceptTcpHandler
的逻辑比较简单,如果done
这个原子变量为1则说明当前服务正在关闭或则已经关闭,新得到的连接无需处理,直接关闭了,反之将这个conn封装成一个客户端对象存到到cliens
中,然后为这个客户端分配一个协程调用客户端的ReadQueryFromClient
处理器读写事件:
func acceptTcpHandler(conn net.Conn) {//the current server is being or has been shut down, and no new connections are being processed.if server.done.Load() == 1 {log.Println("the current service is being shut down. The connection is denied.")_ = conn.Close()}//init the redis client and handles network read and write events.c := &redisClient{conn: conn, argc: 0, argv: make([]string, 0), multibulklen: -1}server.clients.Store(c.string(), c)go readQueryFromClient(c, server.closeClientCh, server.commandCh)}
对应的我们也给出关闭服务端监听和redis客户端连接的核心逻辑,可以看到笔者首先拿到server的监听将其关闭,然后遍历clients
取出每一个客户端连接关闭:
func closeRedisServer() {log.Println("close listen and all redis client")_ = server.listen.Close()server.clients.Range(func(key, value any) bool {client := value.(*redisClient)_ = client.conn.Close()server.clients.Delete(key)return true})}
每个客户端通过readQueryFromClient处理读写请求,一旦收到关闭的事件就会向closeClientCh
发送要关闭的客户端指针,由此我们的协程就会找到这个客户端指针将其conn
关闭,并将其从map
中删除:
//handle client connections that are actively closedgo func(server *redisServer) {for {c := <-server.closeClientChlog.Println("receive close client signal")_ = c.conn.Close()server.clients.Delete(c.string())log.Println("close client successful ", server.clients)}}(&server)
小结
自此,笔者将mini-redis的基本网络连接基调和整体实现思路都讲解完成了,总的来说笔者沿用了大部分redis
的设计思路甚至是命名,只不过go语言有着更好的理念和封装,在此基础上笔者结合两者的优秀设计理念和思路开始着手编写mini-redis
这个项目,希望对你有帮助:
源码地址:https://github.com/shark-ctrl/mini-redis
我是 sharkchili ,CSDN Java 领域博客专家,mini-redis的作者,我想写一些有意思的东西,希望对你有帮助,如果你想实时收到我写的硬核的文章也欢迎你关注我的公众号: 写代码的SharkChili 。
因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 “加群” 即可和笔者和笔者的朋友们进行深入交流。
参考
《redis设计与实现》
这篇关于来聊聊我用go手写redis这件事的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!