使用 Golang 实现的简易 Redis 🔨
使用 Go 语言基于 Redis serialization protocol (RESP) 实现简易的 Redis
开源地址: https://github.com/hcjjj/redis-go
1 | # _ _ |
编译运行:
1 | redis.conf 设置服务器信息、数据库核心数、aof 持久化相关、集群相关 |
实现逻辑
TCP 服务器:
协议解析器:
内存数据库:
持久化流程:
集群架构:
集群指令执行流程:
目录结构
1 | ├── aof # AOF 持久化 |
RESP
Redis 序列化协议规范,**Redis serialization protocol specification**
RESP 是一个二进制安全的文本协议,以行作为单位,客户端和服务器发送的命令或数据一律以 \r\n
(CRLF)作为换行符,RESP 的二进制安全性允许在 key 或者 value 中包含 \r
或者 \n
这样的特殊字符。
二进制安全是指允许协议中出现任意字符而不会导致故障
- 正确回复(Redis → Client)
- 以
+
开头,以 “\r\n” 结尾的字符串形式 - 如:
+OK\r\n
- 以
- 错误回复(Redis → Client)
- 以
-
开头,以 “\r\n” 结尾的字符串形式 - 如:
-Error message\r\n
- 以
- 整数(Redis ⇄ Client)
- 以
:
开头,以 “\r\n” 结尾的字符串形式 - 如:
:123456\r\n
- 以
- 单行字符串(Redis ⇄ Client)
- 以
$
开头,后跟实际发送字节数,以 “\r\n “ 结尾 - “Redis”:
$5\r\nRedis\r\n
- “”:
$0\r\n\r\n
- “Redis\r\ngo”:
$11\r\nRedis\r\ngo\r\n
- 以
- 多行字符串(数组)(Redis ⇄ Client)
- 以
*
开头,后跟成员个数 - 有 3 个成员的数组 [SET, key, value]:
*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n
- 以
支持命令
- PING
- SELECT
- Key 命令集
- DEL
- EXISTS
- FlushDB
- TYPE
- RENAME
- RENAMENX
- KEYS
- String 命令集
- GET
- SET
- SETNX
- GETSET
- STRLEN
- …

测试命令:
- ping
$4\r\nping\r\n
- set key value
*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n
- set ke1 value
*3\r\n$3\r\nSET\r\n$3\r\nke1\r\n$5\r\nvalue\r\n
- select 1
*2\r\n$6\r\nselect\r\n$1\r\n1\r\n
- get key
*2\r\n$3\r\nGET\r\n$3\r\nkey\r\n
- select 2
*2\r\n$6\r\nselect\r\n$1\r\n1\r\n
telnet 需要逐条发送如 $4↩︎ping↩︎
性能测试
1 | ❯ neofetch |
redis-go
1 | ❯ redis-benchmark -h 127.0.0.1 -p 6379 -t set,get -n 10000 -q |
redis
1 | ❯ redis-benchmark -h 127.0.0.1 -p 6379 -t set,get -n 10000 |
排错记录
当客户端主动断开连接的时候服务器报错,
panic: sync: negative WaitGroup counter
waitDone.Add(1)
不小心写成waitDone.Add(0)
,导致后续的waitDone.Done()
出现 panic
imports redis-go/database: import cycle not allowed
- aof.go 文件导包错误,需要的是 “redis-go/interface/database”,而不是 “redis-go/database”
[ERROR][database.go:76] runtime error: index out of range [1] with length 1
- execSelect 方法中的
strconv.Atoi(string(args[0]))
写成了 1
- execSelect 方法中的
语法层面的 “坑”
- Go 的 for 循环的迭代变量都是共享地址
- go1.22 版本之后解决了 for 循环变量共享的问题 ⚠️
- Go 的数组只能用常量来初始化
- Go 的切片有着共享内存的特性
- Go 有类型推断,但是没有自动类型转换
- Go 的 for 循环的迭代变量都是共享地址