一.为什么使用连接池
首先Redis也是一种数据库,它基于C/S模式,因此如果需要使用必须建立连接,C/S模式本身就是一种远程通信的交互模式,因此Redis服务器可以单独作为一个数据库服务器来独立存在。假设Redis服务器与客户端分处在异地,虽然基于内存的Redis数据库有着超高的性能,但是底层的网络通信却占用了一次数据请求的大量时间,因为每次数据交互都需要先建立连接,假设一次数据交互总共用时30ms,超高性能的Redis数据库处理数据所花的时间可能不到1ms,也即是说前期的连接占用了29ms,连接池则可以实现在客户端建立多个链接并且不释放,当需要使用连接的时候通过一定的算法获取已经建立的连接,使用完了以后则还给连接池,这就免去了数据库连接所占用的时间。
二.go-redis里连接池的实现
2.1 go-redis对外提供的接口
type Pooler interface {
NewConn() (*Conn, error)
CloseConn(*Conn) error
Get() (*Conn, error)
Put(*Conn)
Remove(*Conn)
Len() int
IdleLen() int
Stats() *Stats
Close() error
}
包含四个主要模块:
- 建立连接和关闭连接
- 池子里面取Conn的管理
- 监控统计
- 整个Pooler池子的关闭
2.2 Pool初始化
type ConnPool struct {
opt *Options //初始化的配置项
dialErrorsNum uint32 // atomic 连接错误次数
lastDialError error //连接错误的最后一次的错误类型
lastDialErrorMu sync.RWMutex
queue chan struct{} //池子里面空闲的conn的同步channel
connsMu sync.Mutex
conns []*Conn //活跃的active conns
idleConnsMu sync.RWMutex
idleConns []*Conn //空闲的idle conns
poolSize int
idleConnsLen int
stats Stats
_closed uint32 // atomic //池子是否关闭标签
}
var _ Pooler = (*ConnPool)(nil) //接口检查
func NewConnPool(opt *Options) *ConnPool {
p := &ConnPool{
opt: opt,
queue: make(chan struct{}, opt.PoolSize),
conns: make([]*Conn, 0, opt.PoolSize),
idleConns: make([]*Conn, 0, opt.PoolSize),
}
for i := 0; i < opt.MinIdleConns; i++ {
p.checkMinIdleConns()
}
if opt.IdleTimeout > 0 && opt.IdleCheckFrequency > 0 {
go p.reaper(opt.IdleCheckFrequency)
}
return p
}
func newConnPool(opt *Options) *pool.ConnPool {
return pool.NewConnPool(&pool.Options{
Dialer: func(c context.Context) (net.Conn, error) {
return opt.Dialer(c, opt.Network, opt.Addr)
},
PoolSize: opt.PoolSize,
MinIdleConns: opt.MinIdleConns,
MaxConnAge: opt.MaxConnAge,
PoolTimeout: opt.PoolTimeout,
IdleTimeout: opt.IdleTimeout,
IdleCheckFrequency: opt.IdleCheckFrequency,
})
}
//reaper字面意思为收割者,为清理的意思
func (p *ConnPool) reaper(frequency time.Duration) {
ticker := time.NewTicker(frequency)
defer ticker.Stop()
for range ticker.C {
if p.closed() {
break
}
//定时清理无用的conns
n, err := p.ReapStaleConns()
if err != nil {
internal.Logf("ReapStaleConns failed: %s", err)
continue
}
atomic.AddUint32(&p.stats.StaleConns, uint32(n))
}
}
func (p *ConnPool) ReapStaleConns() (int, error) {
var n int
for {
//往channel里面写入一个,表示占用一个任务
p.getTurn()
p.idleConnsMu.Lock()
cn := p.reapStaleConn()
p.idleConnsMu.Unlock()
if cn != nil {
p.removeConn(cn)
}
//处理完了,释放占用的channel的位置
p.freeTurn()
if cn != nil {
p.closeConn(cn)
n++
} else {
break
}
}
return n, nil
}
func (p *ConnPool) reapStaleConn() *Conn {
if len(p.idleConns) == 0 {
return nil
}
//取第一个空闲conn
cn := p.idleConns[0]
//判断是否超时没有人用
if !cn.IsStale(p.opt.IdleTimeout) {
return nil
}
//超时没有人用则从空闲列表里面移除
p.idleConns = append(p.idleConns[:0], p.idleConns[1:]...)
p.idleConnsLen--
p.removeConn(cn)
return cn
}
//判断是否超时的处理
func (cn *Conn) IsStale(timeout time.Duration) bool {
return timeout > 0 && time.Since(cn.UsedAt()) > timeout
}
//移除连接就是一个很简单的遍历
func (p *ConnPool) removeConn(cn *Conn) {
for i, c := range p.conns {
if c == cn {
p.conns = append(p.conns[:i], p.conns[i+1:]...)
if cn.pooled {
p.poolSize--
p.checkMinIdleConns()
}
return
}
}
}
2.3 创建新连接
func (p *ConnPool) _NewConn(ctx context.Context, pooled bool) (*Conn, error) {
cn, err := p.newConn(ctx, pooled)
if err != nil {
return nil, err
}
p.connsMu.Lock()
//创建好加入conns中
p.conns = append(p.conns, cn)
if pooled {
// If pool is full remove the cn on next Put.
if p.poolSize >= p.opt.PoolSize {
cn.pooled = false
} else {
p.poolSize++
}
}
p.connsMu.Unlock()
return cn, nil
}
func (p *ConnPool) newConn(ctx context.Context, pooled bool) (*Conn, error) {
if p.closed() {
return nil, ErrClosed
}
//判断是否一直出错
if atomic.LoadUint32(&p.dialErrorsNum) >= uint32(p.opt.PoolSize) {
return nil, p.getLastDialError()
}
netConn, err := p.opt.Dialer(ctx)
if err != nil {
p.setLastDialError(err)
//dialer出错到限制条件需要tryDial,本质就是retry的dial
if atomic.AddUint32(&p.dialErrorsNum, 1) == uint32(p.opt.PoolSize) {
go p.tryDial()
}
return nil, err
}
cn := NewConn(netConn)
cn.pooled = pooled
return cn, nil
}
func (p *ConnPool) tryDial() {
for {
if p.closed() {
return
}
//不断Dialer直到成功跳出循环
conn, err := p.opt.Dialer(context.Background())
if err != nil {
p.setLastDialError(err)
time.Sleep(time.Second)
continue
}
//跳出循环,Dialer成功,设置dialErrorsNum为0
atomic.StoreUint32(&p.dialErrorsNum, 0)
_ = conn.Close()
return
}
}
2.4 取出链接
func (p *ConnPool) Get(ctx context.Context) (*Conn, error) {
if p.closed() {
return nil, ErrClosed
}
//等待空闲,通过channel实现同步,如果池子满了,则会阻塞等待,超时后返回
err := p.waitTurn(ctx)
if err != nil {
return nil, err
}
for {
p.connsMu.Lock()
cn := p.popIdle()
p.connsMu.Unlock()
if cn == nil {
break
}
//取出空闲的连接,如果过期继续循环
if p.isStaleConn(cn) {
_ = p.CloseConn(cn)
continue
}
atomic.AddUint32(&p.stats.Hits, 1)
return cn, nil
}
atomic.AddUint32(&p.stats.Misses, 1)
newcn, err := p._NewConn(ctx, true)
//创建新连接
if err != nil {
p.freeTurn()
return nil, err
}
return newcn, nil
}
2.5 放回连接
func (p *ConnPool) Put(cn *Conn) {
if !cn.pooled {
p.Remove(cn)
return
}
p.connsMu.Lock()
//放入空闲conns里面
p.idleConns = append(p.idleConns, cn)
p.idleConnsLen++
p.connsMu.Unlock()
//释放写入的channel
p.freeTurn()
}
三.注意事项
基于go实现的redis组件有两个比较出名的组件,一个是gomodule的redigo,另一个是go-redis的redis库。使用redigo的连接池要注意可能会出现问题。当我们从连接池获取了一个连接,但是这时候连接中断了,我们再去使用该连接肯定是有问题的了。go-redis会根据error的信息做连接的重试,而redigo则不会处理,重试需要在调用方做判断。