上文讲到go-redis从连接池中获取连接,然后进行读写操作,再将连接释放回连接池或者从连接池删除无效的连接。本文接着详细看看连接池相关的代码。
func (c *baseClient) getConn(ctx context.Context) (*pool.Conn, error) {
//Limiter是断路器组件或者频率限制组件,后面再详细介绍golang中常用的Limiter
if c.opt.Limiter != nil {
err := c.opt.Limiter.Allow()
if err != nil {
return nil, err
}
}
cn, err := c._getConn(ctx)
if err != nil {
if c.opt.Limiter != nil {
//向Limiter发送本次操作的结果状态
c.opt.Limiter.ReportResult(err)
}
return nil, err
}
return cn, nil
}
getConn调用了_getConn,并且加了频率限制或断路器模式,再看看_getConn
//package pool定义的Conn
type Conn struct {
usedAt int64 // atomic 连接上次使用的时间,用于空闲超时检查
netConn net.Conn //redis连接
rd *proto.Reader
bw *bufio.Writer
wr *proto.Writer
Inited bool //连接是否被初始化,比如密码验证,selectdb等。
pooled bool //连接是否放入连接池
createdAt time.Time //创建的时间,用于检查该连接是否超过MaxConnAge
}
func (c *baseClient) _getConn(ctx context.Context) (*pool.Conn, error) {
//从连接池中获取一个连接
cn, err := c.connPool.Get(ctx)
if err != nil {
return nil, err
}
//连接已被初始化,直接返回
if cn.Inited {
return cn, nil
}
err = internal.WithSpan(ctx, "redis.init_conn", func(ctx context.Context, span trace.Span) error {
return c.initConn(ctx, cn)
})
if err != nil {
c.connPool.Remove(ctx, cn, err)
if err := errors.Unwrap(err); err != nil {
return nil, err
}
return nil, err
}
return cn, nil
}
//从空闲连接池中获取连接或者创建新的连接
func (p *ConnPool) Get(ctx context.Context) (*Conn, error) {
//连接池已被关闭
if p.closed() {
return nil, ErrClosed
}
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
}
//连接从池子中获取,命中数加1
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
}
func (c *baseClient) initConn(ctx context.Context, cn *pool.Conn) error {
//已被初始化
if cn.Inited {
return nil
}
cn.Inited = true
//无需初始化
if c.opt.Password == "" &&
c.opt.DB == 0 &&
!c.opt.readOnly &&
c.opt.OnConnect == nil {
return nil
}
connPool := pool.NewSingleConnPool(c.connPool, cn)
//此处的conn是redis.Conn
conn := newConn(ctx, c.opt, connPool)
//将redis密码认证、选择db、从节点只读放在一个pipeline中一次提交给redis
_, err := conn.Pipelined(ctx, func(pipe Pipeliner) error {
if c.opt.Password != "" {
if c.opt.Username != "" {
pipe.AuthACL(ctx, c.opt.Username, c.opt.Password)
} else {
pipe.Auth(ctx, c.opt.Password)
}
}
if c.opt.DB > 0 {
pipe.Select(ctx, c.opt.DB)
}
if c.opt.readOnly {
pipe.ReadOnly(ctx)
}
return nil
})
if err != nil {
return err
}
//连接建立时的回调
if c.opt.OnConnect != nil {
return c.opt.OnConnect(ctx, conn)
}
return nil
}
//package redis定义的Conn
type conn struct {
baseClient
cmdable
statefulCmdable
hooks // TODO: inherit hooks
}
// Conn is like Client, but its pool contains single connection.
type Conn struct {
*conn
ctx context.Context
}
c.connPool.Get(ctx) 从连接池中获取一个连接:
//connPool定义
type ConnPool struct {
opt *Options //连接池选项
dialErrorsNum uint32 // atomic
lastDialError atomic.Value
queue chan struct{}
connsMu sync.Mutex //操作conns和idleConns、poolSize、idleConnsLen的互斥锁
conns []*Conn //所有连接
idleConns []*Conn //空闲连接
poolSize int //连接池大小
idleConnsLen int //空闲连接个数
stats Stats //连接池的统计信息
_closed uint32 // atomic //也是用于通知空闲连接超时检查子协程结束的标识
closedCh chan struct{} //通过此通道通知空闲超时检查的子协程结束
}
//连接池选项定义
type Options struct {
Dialer func(context.Context) (net.Conn, error)
OnClose func(*Conn) error
PoolSize int //连接池大小
MinIdleConns int //最小的空闲连接数
MaxConnAge time.Duration //连接存在的最大时间,0不受限制
PoolTimeout time.Duration
IdleTimeout time.Duration //连接空闲的最大时间,0不受限制
IdleCheckFrequency time.Duration //空闲连接检查的时间间隔
}
//传入选项生成一个连接池对象
func newConnPool(opt *Options) *pool.ConnPool {
return pool.NewConnPool(&pool.Options{
Dialer: func(ctx context.Context) (net.Conn, error) {
var conn net.Conn
err := internal.WithSpan(ctx, "redis.dial", func(ctx context.Context, span trace.Span) error {
span.SetAttributes(
label.String("db.connection_string", opt.Addr),
)
var err error
//建立连接
conn, err = opt.Dialer(ctx, opt.Network, opt.Addr)
if err != nil {
_ = internal.RecordError(ctx, span, err)
}
return err
})
return conn, err
},
PoolSize: opt.PoolSize,
MinIdleConns: opt.MinIdleConns,
MaxConnAge: opt.MaxConnAge,
PoolTimeout: opt.PoolTimeout,
IdleTimeout: opt.IdleTimeout,
IdleCheckFrequency: opt.IdleCheckFrequency,
})
}
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),
closedCh: make(chan struct{}),
}
p.connsMu.Lock()
//检查连接池中空闲连接是否满足最小空闲连接数,不满足时启动协程创建新连接加入空闲连接池
p.checkMinIdleConns()
p.connsMu.Unlock()
if opt.IdleTimeout > 0 && opt.IdleCheckFrequency > 0 {
go p.reaper(opt.IdleCheckFrequency)
}
return p
}
//检查空闲连接数是否满足要求
func (p *ConnPool) checkMinIdleConns() {
if p.opt.MinIdleConns == 0 {
return
}
for p.poolSize < p.opt.PoolSize && p.idleConnsLen < p.opt.MinIdleConns {
p.poolSize++
p.idleConnsLen++
go func() {
//添加的连接都是未被初始化的,所以才需要Inited标识
err := p.addIdleConn()
if err != nil {
p.connsMu.Lock()
p.poolSize--
p.idleConnsLen--
p.connsMu.Unlock()
}
}()
}
}
reaper子协程主要调用p.ReapStaleConns()
func (p *ConnPool) ReapStaleConns() (int, error) {
var n int
for {
p.getTurn()
p.connsMu.Lock()
cn := p.reapStaleConn()
p.connsMu.Unlock()
p.freeTurn()
if cn != nil {
_ = p.closeConn(cn)
n++
} else {
break
}
}
atomic.AddUint32(&p.stats.StaleConns, uint32(n))
return n, nil
}
func (p *ConnPool) reapStaleConn() *Conn {
if len(p.idleConns) == 0 {
return nil
}
cn := p.idleConns[0]
//是否过期(空闲时间过长或者创建时间过长)连接
if !p.isStaleConn(cn) {
return nil
}
p.idleConns = append(p.idleConns[:0], p.idleConns[1:]...)
p.idleConnsLen--
//将过期连接删除
p.removeConn(cn)
return cn
}
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
}
}
}
在c.baseClient.process时,会多次尝试,所以如果redis server中间挂掉了,下次Get时从idleConns中取空闲连接,操作命令时会出错。然后进行重连,完成操作以后放入空闲队列。