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

相关文章

Go语言实现将中文转化为拼音功能

《Go语言实现将中文转化为拼音功能》这篇文章主要为大家详细介绍了Go语言中如何实现将中文转化为拼音功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 有这么一个需求:新用户入职 创建一系列账号比较麻烦,打算通过接口传入姓名进行初始化。想把姓名转化成拼音。因为有些账号即需要中文也需要英

Go语言使用Buffer实现高性能处理字节和字符

《Go语言使用Buffer实现高性能处理字节和字符》在Go中,bytes.Buffer是一个非常高效的类型,用于处理字节数据的读写操作,本文将详细介绍一下如何使用Buffer实现高性能处理字节和... 目录1. bytes.Buffer 的基本用法1.1. 创建和初始化 Buffer1.2. 使用 Writ

Redis主从/哨兵机制原理分析

《Redis主从/哨兵机制原理分析》本文介绍了Redis的主从复制和哨兵机制,主从复制实现了数据的热备份和负载均衡,而哨兵机制可以监控Redis集群,实现自动故障转移,哨兵机制通过监控、下线、选举和故... 目录一、主从复制1.1 什么是主从复制1.2 主从复制的作用1.3 主从复制原理1.3.1 全量复制

Redis延迟队列的实现示例

《Redis延迟队列的实现示例》Redis延迟队列是一种使用Redis实现的消息队列,本文主要介绍了Redis延迟队列的实现示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习... 目录一、什么是 Redis 延迟队列二、实现原理三、Java 代码示例四、注意事项五、使用 Redi

Redis缓存问题与缓存更新机制详解

《Redis缓存问题与缓存更新机制详解》本文主要介绍了缓存问题及其解决方案,包括缓存穿透、缓存击穿、缓存雪崩等问题的成因以及相应的预防和解决方法,同时,还详细探讨了缓存更新机制,包括不同情况下的缓存更... 目录一、缓存问题1.1 缓存穿透1.1.1 问题来源1.1.2 解决方案1.2 缓存击穿1.2.1

redis-cli命令行工具的使用小结

《redis-cli命令行工具的使用小结》redis-cli是Redis的命令行客户端,支持多种参数用于连接、操作和管理Redis数据库,本文给大家介绍redis-cli命令行工具的使用小结,感兴趣的... 目录基本连接参数基本连接方式连接远程服务器带密码连接操作与格式参数-r参数重复执行命令-i参数指定命

深入理解Redis大key的危害及解决方案

《深入理解Redis大key的危害及解决方案》本文主要介绍了深入理解Redis大key的危害及解决方案,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着... 目录一、背景二、什么是大key三、大key评价标准四、大key 产生的原因与场景五、大key影响与危

Redis主从复制的原理分析

《Redis主从复制的原理分析》Redis主从复制通过将数据镜像到多个从节点,实现高可用性和扩展性,主从复制包括初次全量同步和增量同步两个阶段,为优化复制性能,可以采用AOF持久化、调整复制超时时间、... 目录Redis主从复制的原理主从复制概述配置主从复制数据同步过程复制一致性与延迟故障转移机制监控与维

Go Gorm 示例详解

《GoGorm示例详解》Gorm是一款高性能的GolangORM库,便于开发人员提高效率,本文介绍了Gorm的基本概念、数据库连接、基本操作(创建表、新增记录、查询记录、修改记录、删除记录)等,本... 目录1. 概念2. 数据库连接2.1 安装依赖2.2 连接数据库3. 数据库基本操作3.1 创建表(表关

Redis过期键删除策略解读

《Redis过期键删除策略解读》Redis通过惰性删除策略和定期删除策略来管理过期键,惰性删除策略在键被访问时检查是否过期并删除,节省CPU开销但可能导致过期键滞留,定期删除策略定期扫描并删除过期键,... 目录1.Redis使用两种不同的策略来删除过期键,分别是惰性删除策略和定期删除策略1.1惰性删除策略