redis性能调优一则

本文探讨了如何通过优化Golang中redigo库的使用来提升Redis性能。通过分析conn.Do()、Send()、Flush()、Receive()的使用,作者展示了在循环中使用Send()和批量Flush()能显著提高查询速度,减少了网络传输次数,利用Redis的pipelining特性实现了性能提升。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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<count; 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次网络传输,这差距就显而易见了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值