概述

Redis是我们日常开发中使用的最常见的一种Nosql,是一个key-value存储系统,但是redis不止支持key-value,还自持很多存储类型包括字符串、链表、集合、有序集合和哈希。
在go使用redis中有很多的开源库可以使用,我经常使用的是redigo这个库,它封装很多对redis的api、网络链接和连接池。
分析Redigo之前我觉得需要知道如果不用redigo,我们该如何访问redis。之后才能更加简单方便的理解Redigo是做了一些什么事。

Protocol协议

官方对protocol协议的定义:链接

网络层:

客户端和服务端用通过TCP链接来交互

请求
*<参数数量> CR LF
$<参数 1 的字节数量> CR LF
<参数 1 的数据> CR LF
...
$<参数 N 的字节数量> CR LF
<参数 N 的数据> CR LF
举个例子 get aaa = *2rn$3\r\nget\r\n$3rn$aaarn
每个参数结尾用rn $之后是参数的字节数
这样组成的一串命令通过tcp发送到redis服务端之后就是redis的返回了
返回

Redis的返回有5中情况:

  • 状态回复(status reply)的第一个字节是 "+"
  • 错误回复(error reply)的第一个字节是 "-"
  • 整数回复(integer reply)的第一个字节是 ":"
  • 批量回复(bulk reply)的第一个字节是 "$"
  • 多条批量回复(multi bulk reply)的第一个字节是 "*"

下面按照5中情况各自举一个例子

状态回复
请求: set aaa aaa
回复: +OKrn
错误回复
请求: set aaa
回复: -ERR wrong number of arguments for 'set' commandrn
整数回复
请求:llen list
回复::5rn
批量回复
请求: get aaa
回复: $3rnaaarn
多条批量回复
请求: lrange list 0 -1
回复: *3rn$3\r\naaa\r\n$3rndddrn$3rncccrn

实现

那么我们如何用go来实现不用redis框架,自己请求redis服务。其实也很简单,go提供很方便的net包让我们很容易的使用tcp
先看解析回复方法,封装了一个reply对象:

package clientimport (    "bufio"    "errors"    "fmt"    "net"    "strconv")type Reply struct {    Conn        *net.TCPConn    SingleReply []byte    MultiReply  [][]byte    Source      []byte    IsMulti     bool    Err         error}// 组成请求命令func MultiCommandMarshal(args ...string) string {    var s string    s = "*"    s += strconv.Itoa(len(args))    s += "\r\n"    // 命令所有参数    for _, v := range args {        s += "$"        s += strconv.Itoa(len(v))        s += "\r\n"        s += v        s += "\r\n"    }    return s}// 预读取第一个字节判断是多行还是单行返回 分开处理func (reply *Reply) Reply() {    rd := bufio.NewReader(reply.Conn)    b, err := rd.Peek(1)    if err != nil {        fmt.Println("conn error")    }    fmt.Println("prefix =", string(b))    if b[0] == byte('*') {        reply.IsMulti = true        reply.MultiReply, reply.Err = multiResponse(rd)    } else {        reply.IsMulti = false        reply.SingleReply, err = singleResponse(rd)        if err != nil {            reply.Err = err            return        }    }}// 多行返回 每次读取一行然后调用singleResponse 获取单行数据func multiResponse(rd *bufio.Reader) ([][]byte, error) {    prefix, err := rd.ReadByte()    var result [][]byte    if err != nil {        return result, err    }    if prefix != byte('*') {        return result, errors.New("not multi response")    }    //*3\r\n$1\r\n3\r\n$1\r\n2\r\n$1\r\n    l, _, err := rd.ReadLine()    if err != nil {        return result, err    }    n, err := strconv.Atoi(string(l))    if err != nil {        return result, err    }    for i := 0; i < n; i++ {        s, err := singleResponse(rd)        fmt.Println("i =", i, "result = ", string(s))        if err != nil {            return result, err        }        result = append(result, s)    }    return result, nil}// 获取单行数据 + - : 逻辑相同 $单独处理func singleResponse(rd *bufio.Reader) ([]byte, error) {    var (        result []byte        err    error    )    prefix, err := rd.ReadByte()    if err != nil {        return []byte{}, err    }    switch prefix {    case byte('+'), byte('-'), byte(':'):        result, _, err = rd.ReadLine()    case byte('$'):        // $7\r\nliangwt\r\n        n, _, err := rd.ReadLine()        if err != nil {            return []byte{}, err        }        l, err := strconv.Atoi(string(n))        if err != nil {            return []byte{}, err        }        p := make([]byte, l+2)        rd.Read(p)        result = p[0 : len(p)-2]    }    return result, err}

然后看下如何调用

package mainimport (    "bufio"    "flag"    "fmt"    "log"    "net"    "os"    "strconv"    "strings"    "test/redis/rediscli/client")var host stringvar port stringfunc init() {    // 参数获取 设置有默认值    flag.StringVar(&host, "h", "localhost", "hsot")    flag.StringVar(&port, "p", "6379", "port")}func main() {    flag.Parse()    porti, err := strconv.Atoi(port)    if err != nil {        panic("port is error")    }        tcpAddr := &net.TCPAddr{IP: net.ParseIP(host), Port: porti}    conn, err := net.DialTCP("tcp", nil, tcpAddr)    if err != nil {        log.Println(err)    }    defer conn.Close()    for {        fmt.Printf("%s:%d>", host, porti)        bio := bufio.NewReader(os.Stdin)        input, _, err := bio.ReadLine()        if err != nil {            fmt.Println(err)        }        s := strings.Split(string(input), " ")        req := client.MultiCommandMarshal(s...)        conn.Write([]byte(req))        reply := client.Reply{}        reply.Conn = conn        reply.Reply()        if reply.Err != nil {            fmt.Println("err:", reply.Err)        }        var res []byte        if reply.IsMulti {        } else {            res = reply.SingleReply        }        fmt.Println("result:", string(res), "\nerr:", err)        //fmt.Println(string(p))    }}

总结

上面的代码我们看到根据不同的回复类型,用不同的逻辑解析。
其实所有的redis 处理框架的本质就是封装上面的代码,让我们使用更加方便。当然还有一些其他的功能 使用Lua脚本、发布订阅等等功能。
我觉得要理解redis库 首先要理解Protocol,然后再去看源码 否则你会看到很多你看不懂的逻辑和封装。所以先研究了下Protocol协议并自己实现了一下。