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