来聊聊我用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

相关文章

Knife4j+Axios+Redis前后端分离架构下的 API 管理与会话方案(最新推荐)

《Knife4j+Axios+Redis前后端分离架构下的API管理与会话方案(最新推荐)》本文主要介绍了Swagger与Knife4j的配置要点、前后端对接方法以及分布式Session实现原理,... 目录一、Swagger 与 Knife4j 的深度理解及配置要点Knife4j 配置关键要点1.Spri

go中的时间处理过程

《go中的时间处理过程》:本文主要介绍go中的时间处理过程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录1 获取当前时间2 获取当前时间戳3 获取当前时间的字符串格式4 相互转化4.1 时间戳转时间字符串 (int64 > string)4.2 时间字符串转时间

Go语言中make和new的区别及说明

《Go语言中make和new的区别及说明》:本文主要介绍Go语言中make和new的区别及说明,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录1 概述2 new 函数2.1 功能2.2 语法2.3 初始化案例3 make 函数3.1 功能3.2 语法3.3 初始化

Redis出现中文乱码的问题及解决

《Redis出现中文乱码的问题及解决》:本文主要介绍Redis出现中文乱码的问题及解决,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录1. 问题的产生2China编程. 问题的解决redihttp://www.chinasem.cns数据进制问题的解决中文乱码问题解决总结

Go语言中nil判断的注意事项(最新推荐)

《Go语言中nil判断的注意事项(最新推荐)》本文给大家介绍Go语言中nil判断的注意事项,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录1.接口变量的特殊行为2.nil的合法类型3.nil值的实用行为4.自定义类型与nil5.反射判断nil6.函数返回的

Go语言数据库编程GORM 的基本使用详解

《Go语言数据库编程GORM的基本使用详解》GORM是Go语言流行的ORM框架,封装database/sql,支持自动迁移、关联、事务等,提供CRUD、条件查询、钩子函数、日志等功能,简化数据库操作... 目录一、安装与初始化1. 安装 GORM 及数据库驱动2. 建立数据库连接二、定义模型结构体三、自动迁

Go语言代码格式化的技巧分享

《Go语言代码格式化的技巧分享》在Go语言的开发过程中,代码格式化是一个看似细微却至关重要的环节,良好的代码格式化不仅能提升代码的可读性,还能促进团队协作,减少因代码风格差异引发的问题,Go在代码格式... 目录一、Go 语言代码格式化的重要性二、Go 语言代码格式化工具:gofmt 与 go fmt(一)

Redis的持久化之RDB和AOF机制详解

《Redis的持久化之RDB和AOF机制详解》:本文主要介绍Redis的持久化之RDB和AOF机制,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录概述RDB(Redis Database)核心原理触发方式手动触发自动触发AOF(Append-Only File)核

Redis分片集群、数据读写规则问题小结

《Redis分片集群、数据读写规则问题小结》本文介绍了Redis分片集群的原理,通过数据分片和哈希槽机制解决单机内存限制与写瓶颈问题,实现分布式存储和高并发处理,但存在通信开销大、维护复杂及对事务支持... 目录一、分片集群解android决的问题二、分片集群图解 分片集群特征如何解决的上述问题?(与哨兵模

SpringBoot连接Redis集群教程

《SpringBoot连接Redis集群教程》:本文主要介绍SpringBoot连接Redis集群教程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录1. 依赖2. 修改配置文件3. 创建RedisClusterConfig4. 测试总结1. 依赖 <de