redis性能チューニング一則

11958 ワード

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回のクエリを行い,2番目のメソッドは1番目のメソッドより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接続の構造で、私たちは2人のメンバーがいることを発見しました.それぞれ*bufioです.リーダーと*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
"
) return err }

ここまで、Do()という方法は基本的にSend(),Flush(),Receive()をパッケージ化していることを知っていますが、なぜ2番目のテスト関数は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)です.違いは,Do()のようにsend,flush,recvを絶えず実行するのではなく,複数の命令がバッファに書き込まれることである.バッファに書いた後、Flush()を統一し、命令をネットワークioに全部書きます.redis serverはpipeliningをサポートしているので、ioの中からもう一つReceiveを出せばいいです.このように見ると、1000個の命令は、私は一度だけネットワーク伝送を行いました.Doを用いると,1000回のネットワーク伝送が実行され,この差は明らかである.