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

相关文章

sky-take-out项目中Redis的使用示例详解

《sky-take-out项目中Redis的使用示例详解》SpringCache是Spring的缓存抽象层,通过注解简化缓存管理,支持Redis等提供者,适用于方法结果缓存、更新和删除操作,但无法实现... 目录Spring Cache主要特性核心注解1.@Cacheable2.@CachePut3.@Ca

Redis实现高效内存管理的示例代码

《Redis实现高效内存管理的示例代码》Redis内存管理是其核心功能之一,为了高效地利用内存,Redis采用了多种技术和策略,如优化的数据结构、内存分配策略、内存回收、数据压缩等,下面就来详细的介绍... 目录1. 内存分配策略jemalloc 的使用2. 数据压缩和编码ziplist示例代码3. 优化的

redis-sentinel基础概念及部署流程

《redis-sentinel基础概念及部署流程》RedisSentinel是Redis的高可用解决方案,通过监控主从节点、自动故障转移、通知机制及配置提供,实现集群故障恢复与服务持续可用,核心组件包... 目录一. 引言二. 核心功能三. 核心组件四. 故障转移流程五. 服务部署六. sentinel部署

GO语言短变量声明的实现示例

《GO语言短变量声明的实现示例》在Go语言中,短变量声明是一种简洁的变量声明方式,使用:=运算符,可以自动推断变量类型,下面就来具体介绍一下如何使用,感兴趣的可以了解一下... 目录基本语法功能特点与var的区别适用场景注意事项基本语法variableName := value功能特点1、自动类型推

GO语言中函数命名返回值的使用

《GO语言中函数命名返回值的使用》在Go语言中,函数可以为其返回值指定名称,这被称为命名返回值或命名返回参数,这种特性可以使代码更清晰,特别是在返回多个值时,感兴趣的可以了解一下... 目录基本语法函数命名返回特点代码示例命名特点基本语法func functionName(parameters) (nam

基于Redis自动过期的流处理暂停机制

《基于Redis自动过期的流处理暂停机制》基于Redis自动过期的流处理暂停机制是一种高效、可靠且易于实现的解决方案,防止延时过大的数据影响实时处理自动恢复处理,以避免积压的数据影响实时性,下面就来详... 目录核心思路代码实现1. 初始化Redis连接和键前缀2. 接收数据时检查暂停状态3. 检测到延时过

Go之errors.New和fmt.Errorf 的区别小结

《Go之errors.New和fmt.Errorf的区别小结》本文主要介绍了Go之errors.New和fmt.Errorf的区别,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考... 目录error的基本用法1. 获取错误信息2. 在条件判断中使用基本区别1.函数签名2.使用场景详细对

Redis实现分布式锁全过程

《Redis实现分布式锁全过程》文章介绍Redis实现分布式锁的方法,包括使用SETNX和EXPIRE命令确保互斥性与防死锁,Redisson客户端提供的便捷接口,以及Redlock算法通过多节点共识... 目录Redis实现分布式锁1. 分布式锁的基本原理2. 使用 Redis 实现分布式锁2.1 获取锁

Redis中哨兵机制和集群的区别及说明

《Redis中哨兵机制和集群的区别及说明》Redis哨兵通过主从复制实现高可用,适用于中小规模数据;集群采用分布式分片,支持动态扩展,适合大规模数据,哨兵管理简单但扩展性弱,集群性能更强但架构复杂,根... 目录一、架构设计与节点角色1. 哨兵机制(Sentinel)2. 集群(Cluster)二、数据分片

Go语言连接MySQL数据库执行基本的增删改查

《Go语言连接MySQL数据库执行基本的增删改查》在后端开发中,MySQL是最常用的关系型数据库之一,本文主要为大家详细介绍了如何使用Go连接MySQL数据库并执行基本的增删改查吧... 目录Go语言连接mysql数据库准备工作安装 MySQL 驱动代码实现运行结果注意事项Go语言执行基本的增删改查准备工作