读源码:redigo 的连接池

2,131 阅读6分钟

前面有说到,redigo 不是一个并发安全的 redis 库,它推荐在并发时使用连接池来访问 redis 服务器,redigo 连接池的典型使用方法如下所示:

package main

import (
	"github.com/gomodule/redigo/redis"
	"log"
)

func main() {
	pool := redis.Pool{
		Dial: func() (conn redis.Conn, e error) {
			return redis.Dial("tcp","192.168.1.10:6379")
		},
		MaxIdle:2,
	}
	defer pool.Close()

	conn := pool.Get()
	reply, err := redis.String(conn.Do("SET", "hello", "world"))
	if err != nil {
		log.Println(err)
	}

	log.Println(reply)
	conn.Close()
}

Pool 结构说明

在介绍 pool 几个重要方法的实现之前,我们先来看一下 redis.Pool 结构的一些参数,godoc 的传送门在这里

参数名 说明
Dial 该参数为链接redis的函数,每次从连接池中获取连接时,如果没有空闲的链接,就将会用该函数创建新的链接
TestOnBorrow 测试连接状态的函数
MaxIdle 连接池最大可有的空闲连接数
MaxActive 连接池最大可有的连接数,这个参数通常和下面的Wait参数同时使用
IdleTimeout 空闲连接的超时时间
Wait 当该值为True,并且设置了MaxActive,当已使用的连接数已经达到了MaxActive,那么,Get函数会一直等待,直到有连接可用;如果设置了MaxActive,但是Wait为False,那么,当没有连接可用时,会直接返回 ErrPoolExhausted 的错误。
MaxConnLifetime 连接的生命周期,当调用Get时,会判断连接是否超时,如果超时,会将连接关闭
chInitialized 该值用来判断下面的ch参数是否已经初始化
mu 互斥锁就不需要介绍了
closed 判断连接池是否已经关闭
active 记录连接池中的活跃连接数,该值和下面的idle可以通过方法 Stats() 来查看
ch 该值和Wait参数配合使用,它会被初始化成缓冲区长度和MaxIdle相同的channel,每当使用一个连接时,就会在ch中写入一个值,当使用的连接数和MaxIdle相等时,ch就会阻塞,直到有连接回收
idle 连接池中空闲的连接数,可以通过方法 Stats() 来查看。它是一个链表,它的结构是这样的:type idleList struct {count int; front, back *poolConn;}

Get方法

当新建一个连接池之后,我们使用方法 Get 从连接池中取出一个连接来进行相关的操作,Get 方法的实现如下所示:

func (p *Pool) Get() Conn {
	pc, err := p.get(nil)
	if err != nil {
		return errorConn{err}
	}
	return &activeConn{p: p, pc: pc}
}

如果能成功通过 get 方法获取连接,则返回 activeConn 的实例,否则,返回 errConn 的实例。如果,返回的是 errConn 的实例的话,那么,不论调用什么方法都会直接返回错误 err,十分巧妙的设计。

现在来看一下 get 方法的实现。get 方法的第一段代码如下所示:

if p.Wait && p.MaxActive > 0 {
	p.lazyInit()
	if ctx == nil {
		<-p.ch
	} else {
		select {
		case <-p.ch:
		case <-ctx.Done():
			return nil, ctx.Err()
		}
	}
}

这段代码只有在 p.Wait 为 True 并且设置了 MaxActive 的时候才会执行,它会在 lazyInit(实现如下)中初始化 p.ch ,并且往 p.ch 中写满数据,这样就可以保证能获取 MaxActive 个连接,多余的 Get 请求将会阻塞在 <-p.ch,直到 p.ch 中写入数据,即有连接空闲。

unc (p *Pool) lazyInit() {
	// Fast path.
	if atomic.LoadUint32(&p.chInitialized) == 1 {
		return
	}
	// Slow path.
	p.mu.Lock()
	if p.chInitialized == 0 {
		p.ch = make(chan struct{}, p.MaxActive)
		if p.closed {
			close(p.ch)
		} else {
			for i := 0; i < p.MaxActive; i++ {
				p.ch <- struct{}{}
			}
		}
		atomic.StoreUint32(&p.chInitialized, 1)
	}
	p.mu.Unlock()
}

get 方法中第二段代码是用来判断连接池中是否有超时的空闲连接,先来看一下代码:

if p.IdleTimeout > 0 {
	n := p.idle.count
	for i := 0; i < n && p.idle.back != nil && p.idle.back.t.Add(p.IdleTimeout).Before(nowFunc()); i++ {
		pc := p.idle.back
		p.idle.popBack()
		p.mu.Unlock()
		pc.c.Close()
		p.mu.Lock()
		p.active--
	}
}

因为,redigo 连接池的空闲连接链表 idle 是一个先进后出的链表,所以,back 所指向的空闲连接是相对较老的,所以,这里是从 idle.back 开始判断是否已经超时,如果超时,则将其从链表中取出,并且关闭该连接。

第三段代码是从 idle 中取出空闲的连接:

for p.idle.front != nil {
	pc := p.idle.front
	p.idle.popFront()
	p.mu.Unlock()
	if (p.TestOnBorrow == nil || p.TestOnBorrow(pc.c, pc.t) == nil) &&
		(p.MaxConnLifetime == 0 || nowFunc().Sub(pc.created) < p.MaxConnLifetime) {
		return pc, nil
	}
	pc.c.Close()
	p.mu.Lock()
	p.active--
}

如上所说,它是从 idle 的 front 取出一个空闲的连接。如果有设置测试函数,会先执行测试;如果有设置MaxConnLifetime,那么将会判断连接的生命周期是否已经超过该值。如果能在 idle 中取得符合条件的连接,那么会将其返回,否则会将不符合的连接关闭直到取出符合条件的连接,或者直到 idle 中没有空闲的连接,那么将会继续往下执行。

再往下是两个判断:

if p.closed {
	p.mu.Unlock()
	return nil, errors.New("redigo: get on closed pool")
}

if !p.Wait && p.MaxActive > 0 && p.active >= p.MaxActive {
	p.mu.Unlock()
	return nil, ErrPoolExhausted
}

第一个是判断连接池是否已经关闭。第二个是判断未设置 Wait 但是设置了 MaxActive 的情况下,已创建的连接数是否已经达到了 MaxActive,如果条件满足,则返回错误。

最后一段代码是创建并返回连接:

p.active++
p.mu.Unlock()
c, err := p.Dial()
if err != nil {
	c = nil
	p.mu.Lock()
	p.active--
	if p.ch != nil && !p.closed {
		p.ch <- struct{}{}
	}
	p.mu.Unlock()
}
return &poolConn{c: c, created: nowFunc()}, err

如果创建失败,那么,它会重新往 p.ch 中写入值。因为在第一段代码中从 p.ch 取出了一个值,如果创建失败了不重新写入,那么等于 p.ch 中能缓冲的数据就减少了,从而导致能从连接池中获取的连接就减少了,最后可能就在第一段代码那里死锁了。

连接的回收

在一个不使用连接池的场景中,当调用了 conn.Close() 之后,当前的连接将会被关闭,而在使用连接池的情况下,调用 conn.Close() 会发生什么呢?我们来看一下连接池返回的连接,也就是 activeConn 的 Close 方法的实现:

func (ac *activeConn) Close() error {
	pc := ac.pc
	if pc == nil {
		return nil
	}
	ac.pc = nil

	if ac.state&internal.MultiState != 0 {
		pc.c.Send("DISCARD")
		ac.state &^= (internal.MultiState | internal.WatchState)
	} else if ac.state&internal.WatchState != 0 {
		pc.c.Send("UNWATCH")
		ac.state &^= internal.WatchState
	}
	if ac.state&internal.SubscribeState != 0 {
		pc.c.Send("UNSUBSCRIBE")
		pc.c.Send("PUNSUBSCRIBE")
		// To detect the end of the message stream, ask the server to echo
		// a sentinel value and read until we see that value.
		sentinelOnce.Do(initSentinel)
		pc.c.Send("ECHO", sentinel)
		pc.c.Flush()
		for {
			p, err := pc.c.Receive()
			if err != nil {
				break
			}
			if p, ok := p.([]byte); ok && bytes.Equal(p, sentinel) {
				ac.state &^= internal.SubscribeState
				break
			}
		}
	}
	pc.c.Do("")
	ac.p.put(pc, ac.state != 0 || pc.c.Err() != nil)
	return nil
}

前面一部分代码都是和 redis 服务器进行通信,释放相关的资源,这里主要看 put 方法的实现:

func (p *Pool) put(pc *poolConn, forceClose bool) error {
   p.mu.Lock()
   if !p.closed && !forceClose {
      pc.t = nowFunc()
      p.idle.pushFront(pc)
      if p.idle.count > p.MaxIdle {
         pc = p.idle.back
         p.idle.popBack()
      } else {
         pc = nil
      }
   }

   if pc != nil {
      p.mu.Unlock()
      pc.c.Close()
      p.mu.Lock()
      p.active--
   }

   if p.ch != nil && !p.closed {
      p.ch <- struct{}{}
   }
   p.mu.Unlock()
   return nil
}

首先,它将需要释放的连接插入了 idle 的头部。如果此时空闲的连接数超过了 MaxIdle,那么,将会把尾部的空闲连接即最老的空闲连接取出,并将其关闭。最后,往 p.ch 中写入值,保证在设置了 Wait 的情况下正常运行。

结语

redigo 的连接池就大概分享完了,时间比较匆忙,感觉总结的不是特别好,希望大家能多多斧正。