Redis连接池

一.为什么使用连接池

      首先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
}

包含四个主要模块: 

  1. 建立连接和关闭连接
  2. 池子里面取Conn的管理
  3. 监控统计
  4. 整个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则不会处理,重试需要在调用方做判断。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值