标签:#Redis #RESP #Go语言 #网络编程 #中间件开发 #Socket
📜 一、 破译 RESP:Redis 的通信语言
RESP 是一个基于文本的协议,极其简单且高效。它主要由前缀符号和CRLF (\r\n)组成。
客户端发送给服务端的,永远是一个RESP 数组 (Array),其中包含了命令和参数。
核心数据类型:
| 类型 | 前缀 | 示例 | 含义 |
|---|---|---|---|
| 简单字符串 | + | +OK\r\n | 操作成功 |
| 错误 | - | -Error message\r\n | 报错 |
| 整数 | : | :100\r\n | 数字结果 |
| 多行字符串 | $ | $5\r\nhello\r\n | 字符串 “hello” (长度5) |
| 数组 | * | *2\r\n$3\r\nGET\r\n$1\r\na\r\n | 命令数组["GET", "a"] |
举个栗子:
当你输入SET name gemini时,客户端实际发送的是:
*3\r\n (数组长度 3) $3\r\nSET\r\n (第一个元素,长度3,内容SET) $4\r\nname\r\n (第二个元素,长度4,内容name) $6\r\ngemini\r\n (第三个元素,长度6,内容gemini)🏗️ 二、 架构设计:从 Socket 到存储
我们要实现的 Server 架构非常清晰:
💻 三、 代码实战:Go 语言实现
为了保持代码简洁,我们只实现最核心的SET和GET命令。
1. 编写 RESP 解析器 (Parser)
这是最难的部分。我们需要读取流,解析出*开头的数组。
packagemainimport("bufio""fmt""io""net""strconv""strings""sync")// 内存数据库,简单的并发安全 MapvarkvStore=struct{sync.RWMutex mmap[string]string}{m:make(map[string]string)}funcmain(){// 1. 启动 TCP 监听listener,err:=net.Listen("tcp",":6379")iferr!=nil{fmt.Println("Error starting server:",err)return}fmt.Println("🚀 Mini-Redis is running on port 6379...")for{conn,err:=listener.Accept()iferr!=nil{continue}// 为每个连接启动一个协程gohandleConnection(conn)}}funchandleConnection(conn net.Conn){deferconn.Close()reader:=bufio.NewReader(conn)for{// 2. 解析 RESP 协议 (简化版,只处理 Arrays)// 读取第一个字节,必须是 '*'prefix,err:=reader.ReadByte()iferr==io.EOF{break}ifprefix!='*'{// 这里简单处理,忽略非数组请求或 pingreader.ReadString('\n')continue}// 读取数组长度line,_:=reader.ReadString('\n')arrLen,_:=strconv.Atoi(strings.TrimSpace(line))// 读取命令参数args:=make([]string,0,arrLen)fori:=0;i<arrLen;i++{// 读取多行字符串前缀 '$'reader.ReadByte()// 读取长度line,_=reader.ReadString('\n')strLen,_:=strconv.Atoi(strings.TrimSpace(line))// 读取实际内容buf:=make([]byte,strLen)io.ReadFull(reader,buf)args=append(args,string(buf))// 读取末尾的 CRLFreader.ReadString('\n')}// 3. 执行命令response:=executeCommand(args)// 4. 发送响应conn.Write([]byte(response))}}2. 编写命令执行引擎 (Handler)
这里处理业务逻辑,并返回符合 RESP 格式的字符串。
funcexecuteCommand(args[]string)string{iflen(args)==0{return"-ERR empty command\r\n"}cmd:=strings.ToUpper(args[0])switchcmd{case"PING":return"+PONG\r\n"case"SET":iflen(args)!=3{return"-ERR wrong number of arguments for 'set' command\r\n"}key,val:=args[1],args[2]kvStore.Lock()kvStore.m[key]=val kvStore.Unlock()return"+OK\r\n"// 简单字符串case"GET":iflen(args)!=2{return"-ERR wrong number of arguments for 'get' command\r\n"}key:=args[1]kvStore.RLock()val,ok:=kvStore.m[key]kvStore.RUnlock()if!ok{return"$-1\r\n"// Null Bulk String (表示不存在)}// 返回 Bulk Stringreturnfmt.Sprintf("$%d\r\n%s\r\n",len(val),val)default:returnfmt.Sprintf("-ERR unknown command '%s'\r\n",cmd)}}🎩 四、 见证奇迹时刻
1. 运行你的服务端
go run main.go# 输出: 🚀 Mini-Redis is running on port 6379...2. 使用官方 Redis-CLI 连接
打开一个新的终端窗口:
redis-cli -p6379输入命令测试:
127.0.0.1:6379>PING PONG127.0.0.1:6379>SET user gemini OK127.0.0.1:6379>GET user"gemini"127.0.0.1:6379>GET unknown(nil)成功了!你的程序现在“骗”过了官方客户端,它以为自己在跟真正的 Redis 对话。
🔍 五、 为什么这很重要?
掌握 RESP 协议解析,意味着你拥有了开发以下系统的能力:
- Redis Proxy (代理):像 Twemproxy 或 Codis 一样,拦截客户端请求,实现分片(Sharding)或读写分离。
- 兼容 Redis 的新数据库:比如你可以用 RocksDB 做底层存储,外面套一层 RESP 协议,做一个持久化的“大容量 Redis”。
- 流量录制与回放:通过解析 RESP 流量,分析热 Key 或进行故障复现。
🎯 总结
Redis 之所以流行,除了它快,还因为它简单的协议设计。RESP 协议通过简单的文本规则,实现了高性能与易读性的平衡。
今天的代码虽然简单,但它已经包含了一个数据库中间件的雏形:监听 -> 解析 -> 路由 -> 执行 -> 响应。
Next Step:
目前的解析器是同步阻塞的,效率较低。
尝试引入 Go 的bufio的Scanner机制,或者尝试支持PIPELINE(一次发送多条命令),这将极大提升你的 Server 吞吐量。