来聊聊我用go手写redis这件事

2024-09-02 09:44

本文主要是介绍来聊聊我用go手写redis这件事,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

写在文章开头

网上有看过一些实现redis的项目,要么完全脱离go语言的理念,要么又完全去迎合c的实现理念,也不是说这些项目写的不好,只能说不符合笔者所认为的那种"平衡",于是整理了一段时间的设计稿,自己尝试着用go语言写了一版"有redis味道"mini-redis

在这里插入图片描述

截至目前,笔者已经完成了redis服务端和客户端交互的基本通信架构和实现基调,如下所示,可以看到笔者已经实现了pingcommand指令:

127.0.0.1:6379> ping
PONG
127.0.0.1:6379> command
1) "COMMAND"
2) "PING"

后续也会一直沿用命令模式的基调不断完善指令即可,而这篇文章算是笔者的一篇引子,也算是从笔者的设计稿中整理出来的一篇个人实现的思路的复盘,希望这个系列对CGoJava开发能有一些灵感。

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进行抽象,在设计之初笔者也沿用这个结构体,为了把握核心脉络笔者只用到了如下几个字段:

  1. ip:记录redis的ip地址
  2. port:记录redis服务端的端口号
  3. clients:记录每一个与redis服务端建立的客户端连接信息,原生redis因为通过单线程epoll处理连接和读写事件,所以维护客户端的数据结构采用链表。而笔者所实现的mini-redis涉及多协程,所以为了保证并发安全采用sync.Map,通过客户端信息序列化作为key,客户端指针作为value进行管理。

在这里插入图片描述

C语言通过redisClient结构体记录与服务端建立连接的客户端文件描述符fd等信息,而go语言对于这些传输层的概念都做了封装,我们只能拿到封装好的连接对象net.Conn,所以目前为止,笔者的的客户端之抽象了连接字段。

在这里插入图片描述

实现中的注意事项

了解整体的结构设计,接下来就是对整个系统的交互边界的设计了,包括:

  1. 程序关闭,如何优雅关闭监听和客户端连接。
  2. 关闭期间收到客户端连接怎么办?
  3. 处理连接遇到异常怎么办?

先来回答第一个问题,按照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指针直接交给serverlisten

//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

我是 sharkchiliCSDN Java 领域博客专家mini-redis的作者,我想写一些有意思的东西,希望对你有帮助,如果你想实时收到我写的硬核的文章也欢迎你关注我的公众号: 写代码的SharkChili
因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 “加群” 即可和笔者和笔者的朋友们进行深入交流。

在这里插入图片描述

参考

《redis设计与实现》

这篇关于来聊聊我用go手写redis这件事的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1129618

相关文章

Redis消息队列实现异步秒杀功能

《Redis消息队列实现异步秒杀功能》在高并发场景下,为了提高秒杀业务的性能,可将部分工作交给Redis处理,并通过异步方式执行,Redis提供了多种数据结构来实现消息队列,总结三种,本文详细介绍Re... 目录1 Redis消息队列1.1 List 结构1.2 Pub/Sub 模式1.3 Stream 结

SpringBoot中配置Redis连接池的完整指南

《SpringBoot中配置Redis连接池的完整指南》这篇文章主要为大家详细介绍了SpringBoot中配置Redis连接池的完整指南,文中的示例代码讲解详细,具有一定的借鉴价值,感兴趣的小伙伴可以... 目录一、添加依赖二、配置 Redis 连接池三、测试 Redis 操作四、完整示例代码(一)pom.

go 指针接收者和值接收者的区别小结

《go指针接收者和值接收者的区别小结》在Go语言中,值接收者和指针接收者是方法定义中的两种接收者类型,本文主要介绍了go指针接收者和值接收者的区别小结,文中通过示例代码介绍的非常详细,需要的朋友们下... 目录go 指针接收者和值接收者的区别易错点辨析go 指针接收者和值接收者的区别指针接收者和值接收者的

Redis在windows环境下如何启动

《Redis在windows环境下如何启动》:本文主要介绍Redis在windows环境下如何启动的实现方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录Redis在Windows环境下启动1.在redis的安装目录下2.输入·redis-server.exe

Go 语言中的select语句详解及工作原理

《Go语言中的select语句详解及工作原理》在Go语言中,select语句是用于处理多个通道(channel)操作的一种控制结构,它类似于switch语句,本文给大家介绍Go语言中的select语... 目录Go 语言中的 select 是做什么的基本功能语法工作原理示例示例 1:监听多个通道示例 2:带

Redis实现延迟任务的三种方法详解

《Redis实现延迟任务的三种方法详解》延迟任务(DelayedTask)是指在未来的某个时间点,执行相应的任务,本文为大家整理了三种常见的实现方法,感兴趣的小伙伴可以参考一下... 目录1.前言2.Redis如何实现延迟任务3.代码实现3.1. 过期键通知事件实现3.2. 使用ZSet实现延迟任务3.3

Redis分片集群的实现

《Redis分片集群的实现》Redis分片集群是一种将Redis数据库分散到多个节点上的方式,以提供更高的性能和可伸缩性,本文主要介绍了Redis分片集群的实现,具有一定的参考价值,感兴趣的可以了解一... 目录1. Redis Cluster的核心概念哈希槽(Hash Slots)主从复制与故障转移2.

Go标准库常见错误分析和解决办法

《Go标准库常见错误分析和解决办法》Go语言的标准库为开发者提供了丰富且高效的工具,涵盖了从网络编程到文件操作等各个方面,然而,标准库虽好,使用不当却可能适得其反,正所谓工欲善其事,必先利其器,本文将... 目录1. 使用了错误的time.Duration2. time.After导致的内存泄漏3. jsO

go中空接口的具体使用

《go中空接口的具体使用》空接口是一种特殊的接口类型,它不包含任何方法,本文主要介绍了go中空接口的具体使用,具有一定的参考价值,感兴趣的可以了解一下... 目录接口-空接口1. 什么是空接口?2. 如何使用空接口?第一,第二,第三,3. 空接口几个要注意的坑坑1:坑2:坑3:接口-空接口1. 什么是空接

Redis 中的热点键和数据倾斜示例详解

《Redis中的热点键和数据倾斜示例详解》热点键是指在Redis中被频繁访问的特定键,这些键由于其高访问频率,可能导致Redis服务器的性能问题,尤其是在高并发场景下,本文给大家介绍Redis中的热... 目录Redis 中的热点键和数据倾斜热点键(Hot Key)定义特点应对策略示例数据倾斜(Data S