go-redis源码分析(二)

2,557 阅读4分钟

上文讲到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中取空闲连接,操作命令时会出错。然后进行重连,完成操作以后放入空闲队列。