redis性能调优一则

redis作为提升web服务端数据交互能力的重要利器,其本身也有开销,为了让redis变得更快,有必要对和redis交互的地方进行性能优化。

今天说一下golang中比较著名的一个redis库—-redigo。它的conn.Do()、Send()、Flush()、Receive()的合理使用是很有必要的。

先上一个我本地测试的例子:

func main(){

    _=InitRedis(10,"127.0.0.1","6379","requirepass",false) //初始化redis,这里就不细写了

    GetRedisKey()
    GetRedisKey2()

}

func GetRedisKey()  {
    now:=time.Now()
    conn := redisPool.Get()
    defer conn.Close()

    for i:=0;i<1000;i++{ //做1000次get
        key :=  "1125"+"test"+strconv.Itoa(i)
        //_, err := conn.Do("set", key,"testValue") 这个是之前set
        _, err := redis.String(conn.Do("get", key)) //执行get,并获取结果
        if err!=nil {
            fmt.Println(err)
        }
        //fmt.Println(result)
    }
    finish1:=time.Since(now) //计时
    fmt.Println(finish1)
}

func GetRedisKey2()  {
    now:=time.Now()
    conn := redisPool.Get()
    defer conn.Close()
    var count int
    for i:=0;i<1000;i++{ //做1000次get
        key :=  "1125"+"test2_"+strconv.Itoa(i)
        //err := conn.Send("set", key,"testValue")之前set
        err := conn.Send("get",key)  //注意这里是send,不是Do了
        if err!=nil {
            fmt.Println(err)
        }
        count++
    }

    err := conn.Flush()   //发送指令
    if err != nil {
        fmt.Println(err)
    }

    for i:=0 ; i
        _, err := redis.String(conn.Receive()) //获取get结果
        if err != nil {
            fmt.Println(err)
        }
    }

    finish2:=time.Since(now)  //计时
    fmt.Println(finish2)

}

实验结果:

80.0561ms   //GetRedisKey()运行耗时
4.0033ms    //GetRedisKey2()运行耗时

结果很明显,同样是做了1000次查询,第二个方法比第一个方法快了20倍。这是为什么呢?接下来说明其中原理。

这个时候要看一看redigo的源码了,先看这个conn的结构:

type conn struct { // Shared mu sync.Mutex pending int err error conn net.Conn // Read readTimeout time.Duration br *bufio.Reader // Write writeTimeout time.Duration bw *bufio.Writer // Scratch space for formatting argument length. // '*' or '$', length, "\r\n" lenScratch [32]byte // Scratch space for formatting integers and floats. numScratch [40]byte }

这个是redis连接的结构,我们发现有两个成员,分别是*bufio.Reader和*bufio.Writer。然后再来看这个Do():

func (c *conn) Do(cmd string, args ...interface{}) (interface{}, error) {
    c.mu.Lock()
    pending := c.pending
    c.pending = 0
    c.mu.Unlock()

    if cmd == "" && pending == 0 {
        return nil, nil
    }

    if c.writeTimeout != 0 {
        c.conn.SetWriteDeadline(time.Now().Add(c.writeTimeout))
    }

    if cmd != "" {
        if err := c.writeCommand(cmd, args); err != nil {  //将指令写入到一个地方去
            return nil, c.fatal(err)
        }
    }

    if err := c.bw.Flush(); err != nil {   //将缓冲取出,放到io.Writer中,看到这句代码,我们就应该知道,上面那条c.writeCommand()应该是把指令放到缓冲里了。
        return nil, c.fatal(err)
    }

    if c.readTimeout != 0 {
        c.conn.SetReadDeadline(time.Now().Add(c.readTimeout))
    }

    if cmd == "" {
        reply := make([]interface{}, pending)
        for i := range reply {
            r, e := c.readReply()  
            if e != nil {
                return nil, c.fatal(e)
            }
            reply[i] = r
        }
        return reply, nil
    }

    var err error
    var reply interface{}
    for i := 0; i <= pending; i++ {
        var e error
        if reply, e = c.readReply(); e != nil {   //读redis server返回的数据
            return nil, c.fatal(e)
        }
        if e, ok := reply.(Error); ok && err == nil {
            err = e
        }
    }
    return reply, err
}

为了一探究竟,看了c.writeCommand()的实现:

func (c *conn) writeCommand(cmd string, args []interface{}) error {
    c.writeLen('*', 1+len(args))
    if err := c.writeString(cmd); err != nil {  //再进一步看这个方法,见下面那段
        return err
    }
    for _, arg := range args {
        if err := c.writeArg(arg, true); err != nil {
            return err
        }
    }
    return nil
}
func (c *conn) writeString(s string) error {
    c.writeLen('$', len(s))
    c.bw.WriteString(s)     //果然调用了bufio的WriteString()方法,把指令都写到了缓冲中
    _, err := c.bw.WriteString("\r\n")
    return err
}

到这里,我们知道,Do()这个方法基本上是包办了Send(),Flush(),Receive(),那为什么第二个测试函数会比Do()快这么多呢?原因就是,我for循环执行了多次Send(),目的就是把多条要执行的指令写到缓冲中。

func (c *conn) Send(cmd string, args ...interface{}) error {
    c.mu.Lock()
    c.pending += 1
    c.mu.Unlock()
    if c.writeTimeout != 0 {
        c.conn.SetWriteDeadline(time.Now().Add(c.writeTimeout))
    }
    if err := c.writeCommand(cmd, args); err != nil {
        return c.fatal(err)
    }
    return nil
}

这是Send()的源码,其实和Do()一开始做的事情一样,都是c.writeCommand(cmd, args)。

区别就在于,我多次执行Send(),是多条指令写到缓冲中,而不是像Do()那样,不断的执行send,flush,recv。写到缓冲之后,我再统一Flush(),把指令全写到网络io中。因为redis server支持pipelining,我再从io中一个一个Receive出来即可。这样看,1000条指令,我只进行了一次网络传输。而用Do,则执行了1000次网络传输,这差距就显而易见了。

你可能感兴趣的:(golang,redis)