作者:HDT3213
今天给大家带来的开源项目是 Godis:一个用 Go 语言实现的 redis 服务器。支持:
你或许不需要自己实现 Redis 服务,但你是否厌烦了每天都是写增删改查的业务代码,想提高编程水平试图从零写个项目打开 IDE 却发现无从下手?
动手造轮子一定是提高编程能力的好办法,下面就带大家用 Go 从零开始写一个 Redis 服务器(Godis),从中你将学到:
千万不要担心内容太难,学不会或者没有 Go 语言基础!!虽然示例代码是 Go 但不会影响你理解 Redis 的原理和底层协议以及高性能的秘密。而且作者为了照顾到广大读者,对技术的讲解做了优化。示例代码在原项目基础上做了简化,并逐行地加了注释。如果是高级玩家,请直接访问项目阅读源码:
https://github.com/HDT3213/godis
下面让我们一起拨开 Redis 的迷雾。
众所周知 Redis 是 C/S 模型,使用 TCP 协议进行通信。接下来就从实现 TCP 服务端开始。作为广泛用于服务端的编程语言 Golang 提供了非常简洁的 TCP 接口,所以实现起来十分方便。示例代码:
func ListenAndServe(address string) {
// 绑定监听地址
listener, err := net.Listen("tcp", address)
if err != nil {
log.Fatal(fmt.Sprintf("listen err: %v", err))
}
defer listener.Close()
log.Println(fmt.Sprintf("bind: %s, start listening...", address))
for {
// Accept 会一直阻塞直到有新的连接建立或者listen中断才会返回
conn, err := listener.Accept()
if err != nil {
// 通常是由于listener被关闭无法继续监听导致的错误
log.Fatal(fmt.Sprintf("accept err: %v", err))
}
// 开启新的 goroutine 处理该连接
go Handle(conn)
}
}
func Handle(conn net.Conn) {
reader := bufio.NewReader(conn)
for {
// ReadString 会一直阻塞直到遇到分隔符 'n'
// 遇到分隔符后 ReadString 会返回上次遇到分隔符到现在收到的所有数据
// 若在遇到分隔符之前发生异常, ReadString 会返回已收到的数据和错误信息
msg, err := reader.ReadString('n')
if err != nil {
// 通常遇到的错误是连接中断或被关闭,用io.EOF表示
if err == io.EOF {
log.Println("connection close")
} else {
log.Println(err)
}
return
}
b := []byte(msg)
// 将收到的信息发送给客户端
conn.Write(b)
}
}
func main() {
ListenAndServe(":8000")
}
至此只用了 40 行代码就搞定服务端啦!启动上面的 TCP 服务后,在终端中输入 telnet 127.0.0.1 8000 就可以连接到刚写好的服务器,它会将你发送的消息原样返回给你(所以请不要骂它):
这个 TCP 服务器的非常简单,主协程调用 accept 函数来监听端口,接受新连接后开启一个 Goroutine 来处理它。这种简单的阻塞 IO 模型有些类似于早期的 Tomcat/Apache 服务器。
阻塞 IO 模型是使用一个线程处理一个连接,在没有收到新数据时监听线程处于阻塞状态,直到数据就绪后线程被唤醒进行处理。因为阻塞 IO 模型需要开启大量线程并且频繁地进行上下文切换,所以它的效率很低。而 Redis 使用的 epoll 技术(IO 多路复用)用一个线程处理大量连接,极大地提高了吞吐量。那么我们的 TCP 服务器会比 Redis 慢很多吗?
当然不会,Golang 利用 Goroutine 调度开销远远小于线程调度开销的优势封装出 goroutine-per-connection 风格的极简接口,而且 net/tcp 库将 epoll 封装成了阻塞 IO 的样子,在享受 epoll 高性能的同时避免了原生 epoll 接口所需的复杂异步代码。
在作者的电脑上 Redis 每秒可以响应 10.6k 个 PING 命令,而 Godis(完整代码) 的吞吐量为 9.2 kqps 相差并不大。想了解更多 Golang 高性能的㊙️密,可以搜索 go netpoller 或者 go 语言 网络轮询器 关键字
另外,合格的 TCP 的服务器在关闭的时候不应该一停了之,而需要完成响应已接收的请求、释放 TCP 连接等必要的清理工作。这个功能我们一般称为 优雅关闭 或者 graceful shutdown,优雅关闭步骤:
优雅关闭的代码比较多,这里就不完整贴出了。
在解决完通信后,下一步就是搞清楚 Redis 的协议,其实就是一套序列化协议类似 JSON、Protocol Buffers,你看底层其实也就是一些基础的知识。
自 Redis 2.0 以后的通信统一为 RESP 协议(REdis Serialization Protocol),该协议易于实现不仅可以高效的被程序解析,还能够被人类读懂容易调试。
RESP 是一个二进制安全的文本协议,工作于 TCP 协议上。RESP 以行作为单位,客户端和服务器发送的命令或数据一律以 rn(CRLF)作为换行符。
二进制安全是指允许协议中出现任意字符而不会导致故障。比如 C 语言的字符串以