redis-go源码阅读笔记

底层数据结构

连接池pool

// redis连接池
type Pool struct {
	*pool.Slice  // 核心是Slice结构体
	c *Config // 连接池配置
		type Config struct {
			*pool.Config // 包含连接池配置,下面有详解

			Name         string
			Proto        string
			Addr         string
			Auth         string
			DialTimeout  xtime.Duration  // 请求拨号超时时长
			ReadTimeout  xtime.Duration  // 底层net.Conn读数据超时时长
			WriteTimeout xtime.Duration  // 底层net.Conn写数据超时时长
			SlowLog      xtime.Duration  // 慢查询阈值,默认250ms
		}
	statfunc func(ctx context.Context, name, addr, cmd string, t time.Time, err error) func() // 数据统计函数
}

// Slice .
type Slice struct {
	New  func(ctx context.Context) (io.Closer, error)  // 创建新连接函数
	stop func()

	mu           sync.Mutex
	freeItem     []*item  // 空闲连接池
	itemRequests map[uint64]chan item  // 记录正在请求连接池获取连接的协程及其req channel; 请求如果不能立即获取连接,会在这里新建一个chan,等待新连接写入
	nextRequest  uint64 // 递增值,记录当前等待连接的位置
	active       int // 当前活跃连接数

	openerCh  chan struct{}  // 用于接收新建连接信号的通道
	closed    bool  // 当前连接池是否关闭
	cleanerCh chan struct{}

	conf *Config // 连接池的配置信息,包括: 
		type Config struct {
			Active int // 同一时刻开启的最大连接数,包括Idle
			Idle int   // 连接池保持的最大空闲连接数, len(freeItem)
			IdelTimeout xtime.Duration // 连接空闲最大时长;空闲连接超过这个时间后会被清理;从freeItem中获取额连接,若存在时间超过该值,也会被销毁(这个很关键)
			WaitTimeout xtime.Duration // 获取连接等待时间;当连接池活跃连接数已满(p.active >= p.conf.Active),Get()等待时间
			Wait bool // Get()是否等待连接;Wait = true,等待直到ctx超时;Wait = false,直接返回错误;Wait配置优先级低于WaitTimeout 
		}
}

连接conn

// 应用层使用的连接,Pool.Get()新建此数据结构并返回
type pooledConnection struct {
	p     *Pool
	rc    Conn  // raw conn, 一般为traceConn
	c     Conn  // 带超时的conn
	state int
	ctx   context.Context  // 应用层传入的ctx, Get(ctx)

	now  time.Time  // 好像固定为 beginTime, _ = time.Parse("2006-01-02 15:04:05", "2006-01-02 15:04:05")
	cmds []string
}

// pool中的实体,在底层Conn的基础上,增加日志等功能
type traceConn struct {
	tr trace.Trace
	trPipe trace.Trace

	connTags []trace.Tag // 连接值,包括ip,端口等信息

	Conn // 底层redis连接
	pending int
	
	slowLogThreshold time.Duration  // 慢日志阈值
}

// 底层Redis连接
type conn struct {
	mu      sync.Mutex
	pending int
	err     error
	conn    net.Conn  // 进行网络传输的net连接

	ctx context.Context

	readTimeout time.Duration  // 读buffer + 读超时时间
	br          *bufio.Reader

	writeTimeout time.Duration // 写buffer + 写超时时间
	bw           *bufio.Writer

	lenScratch [32]byte  // 还不太清楚,应该是用于解析resp.Body时使用的一些特殊字符等
	numScratch [40]byte
}

连接池 - 初始化 - NewPool()

// 初始化缓存池
func NewPool(c *Config, options ...DialOption) (p *Pool) {
	...
	// 初始化Slice
	p1 := pool.NewSlice(c.Config)
	// 定义新建连接函数
	p1.New = func(ctx context.Context) (io.Closer, error) {
		conn, err := Dial(c.Proto, c.Addr, ops...)
		if err != nil {
			return nil, err
		}
		return &traceConn{
			Conn:             conn,
			connTags:         []trace.Tag{trace.TagString(trace.TagPeerAddress, c.Addr)},
			slowLogThreshold: time.Duration(c.SlowLog),
		}, nil
	}
	p = &Pool{Slice: p1, c: c, statfunc: pstat}
	return
}

// 新建Slice
func NewSlice(c *Config) *Slice {
	ctx, cancel := context.WithCancel(context.Background())
	// 意味着,空闲连接池初始为空
	p := &Slice{
		conf:         c,
		stop:         cancel,
		itemRequests: make(map[uint64]chan item),
		openerCh:     make(chan struct{}, 1000000),
	}
	// 启动cleaner
	p.startCleanerLocked(time.Duration(c.IdleTimeout))
	// 启动opener
	go p.itemOpener(ctx)
	return p
}

在这里插入图片描述

Attention:官方不建议继续使用NewPool()获取连接池,可以使用NewRedis()代替。本质上没太大区别,只是Do()函数增加获取连接 + 释放连接的逻辑,应用层代码会简洁一些

连接池 - 获取连接 - Get()

在这里插入图片描述

获取连接这里有几个优先级:

  1. 先从空闲连接队列p.freeItem中获取,取到直接返回连接
  2. freeItem中没有,要看此时活跃连接数是否已达上线,未达上线则新建一个连接
    • 注意:freeItem中的空闲连接也在活跃连接p.active的统计中,即freeItem中的连接也是活跃连接
  3. 如果活跃连接数已达到上线,此时需要用到waitTimeout和wait两个配置参数,前者优先级更高
    • waitTimeout,表示连接数已达到上限时,Get()的阻塞时间(具体阻塞时间会在waitTimeout和ctx.timeout中取较小值,此时会报错context timeout,而非pool exhauted)
    • waitTimeout未设置时,wait配置生效,wait=true,表示Get()会一直阻塞,直到有连接给它,或者ctx超时
    • 若waitTimeout、wait均未设置,Get()直接抛错pool Exhausted

连接池 - 连接释放 - Put()

// 连接释放
func (p *Slice) Put(ctx context.Context, c io.Closer, forceClose bool) error {
	p.mu.Lock()
	defer p.mu.Unlock()
	// 强制关闭,p.active--, 连接关闭
	if forceClose {
		p.release()
		return c.Close()
	}
	// 正常情况下,归还连接到空闲队列或直接给其他等待连接的协程,如果操作失败,则直接关闭连接
	added := p.putItemLocked(c)
	if !added {
		p.active--
		return c.Close()
	}
	return nil
}

// 归还连接逻辑
func (p *Slice) putItemLocked(c io.Closer) bool {
	if p.closed {
		return false
	}
	// 如果活跃连接数比配置要大,则直接关闭连接
	if p.conf.Active > 0 && p.active > p.conf.Active {
		return false
	}
	// 重新封装item,更新连接的创建时间
	// 重要: 每次连接被释放后,会重置其创建时间
	i := item{
		c:         c,
		createdAt: nowFunc(),
	}
	// 先看是否有协程在等待连接,如果有,直接给他,传入它的等待通道req
	if l := len(p.itemRequests); l > 0 {
		var req chan item
		var reqKey uint64
		for reqKey, req = range p.itemRequests {
			break
		}
		delete(p.itemRequests, reqKey) // Remove from pending requests.
		req <- i
		return true
	// 再看空闲队列是否已满, 没满p.conf.Idle > len(p.freeItme),则放入空闲队列,否则直接释放
	} else if !p.closed && p.maxIdleItemsLocked() > len(p.freeItem) {
		p.freeItem = append(p.freeItem, &i)
		return true
	}
	return false
}

连接 - 新建 - New()

// 在初始化连接池时,定义了新建连接函数
// 此新建的连接,是pool中存储的数据结构,非应用层使用的结构
// 应用层使用Get()获取连接,会在此返回的基础上封装成poolConnectedConn
func NewPool() () {
	...
	p1 := pool.NewSlice(c.Config)
	...
	p1.New = func(ctx context.Context) (io.Closer, error) {
		conn, err := Dial(c.Proto, c.Addr, ops...)
		if err != nil {
			return nil, err
		}
		return &traceConn{
			Conn:             conn,
			connTags:         []trace.Tag{trace.TagString(trace.TagPeerAddress, c.Addr)},
			slowLogThreshold: time.Duration(c.SlowLog),
		}, nil
	}
}

// 新建底层Redis连接,包括新建网络连接 + 定义网络超时
func Dial(network, address string, options ...DialOption) (Conn, error) {
	do := dialOptions{
		dial: net.Dial,
	}
	...
	// 这里会增加了几个Timeout
	...

	netConn, err := do.dial(network, address)
	if err != nil {
		return nil, errors.WithStack(err)
	}
	c := &conn{
		conn:         netConn,
		bw:           bufio.NewWriter(netConn),
		br:           bufio.NewReader(netConn),
		readTimeout:  do.readTimeout,
		writeTimeout: do.writeTimeout,
	}
	... 
	return c, nil
}

连接 - 执行 - Do()

// 应用层执行该Do函数
func (pc *pooledConnection) Do(commandName string, args ...interface{}) (reply interface{}, err error) {
	now := time.Now()
	ci := LookupCommandInfo(commandName)
	pc.state = (pc.state | ci.Set) &^ ci.Clear
	// 核心是这里,会调用其内部带超时时间的traceConn
	reply, err = pc.c.Do(commandName, args...)
	// 这里像是prometheus的数据上报,配置业务层Redis监控
	if pc.p.statfunc != nil {
		pc.p.statfunc(pc.ctx, pc.p.c.Name, pc.p.c.Addr, commandName, now, err)()
	}
	return
}

// traceConn在底层Conn.Do的基础上,增加打印慢日志
func (t *traceConn) Do(commandName string, args ...interface{}) (reply interface{}, err error) {
	statement := getStatement(commandName, args...)
	defer t.slowLog(statement, time.Now())
	// 源码初始化时,没有对t.tr的定义,所以Do应该只会走到这个if里,直接执行底层Conn.Do
	if t.tr == nil {
		return t.Conn.Do(commandName, args...)
	}
	...
}

// 底层Conn.Do 
func (c *conn) Do(cmd string, args ...interface{}) (reply interface{}, err error) {
	// 往net.Conn的写buffer里写入cmd, 这里有writeTimeout超时限制
	if cmd != "" {
		err = c.writeCommand(cmd, args)
	}
	...
	// 设置写超时readTimeout限制
	if c.readTimeout != 0 {
		c.conn.SetReadDeadline(shrinkDeadline(c.ctx, c.readTimeout))
	}
	// 从net.Conn的readBuffer中读取和解析内容,并返回
	for i := 0; i <= pending; i++ {
		var e error
		if reply, e = c.readReply(); e != nil {
			return nil, c.fatal(e)
		}
		if e, ok := reply.(Error); ok && err == nil {
			err = e
		}
	}
	return reply, err
}

// 底层网络连接对readTimeOut和writeTimeout的解释
type net.Conn interface {
	// SetReadDeadline sets the deadline for future Read calls
	// and any currently-blocked Read call.
	// A zero value for t means Read will not time out.
	SetReadDeadline(t time.Time) error

	// SetWriteDeadline sets the deadline for future Write calls
	// and any currently-blocked Write call.
	// Even if write times out, it may return n > 0, indicating that
	// some of the data was successfully written.
	// A zero value for t means Write will not time out.
	SetWriteDeadline(t time.Time) error
}

连接 - 执行 - SlowLog()

// redis sdk的慢日志在traceConn中记录,下面是traceConn在执行Do时的慢日志逻辑代码
// 记录时间即:底层Conn.Do()的执行时间
func (t *traceConn) Do(commandName string, args ...interface{}) (reply interface{}, err error) {
	statement := getStatement(commandName, args...)
	defer t.slowLog(statement, time.Now())
	if t.tr == nil {
		return t.Conn.Do(commandName, args...)
	}
}

// 执行时间大于慢日志阈值时,记录慢查询Warn日志
// 其中,slowLogThreshold即为配置文件中SlowLog的值,默认为250ms
func (t *traceConn) slowLog(statement string, now time.Time) {
	du := time.Since(now)
	if du > t.slowLogThreshold {
		log.Warn("%s slow log statement: %s time: %v", _tracePeerService, statement, du)
	}
}

连接 - 执行 - Close()

// 应用层执行该Close函数
func (pc *pooledConnection) Close() error {
	// 一些统计逻辑,不太懂,跳过
	...
	// 关键逻辑,将连接放回连接池
	pc.p.Slice.Put(context.Background(), pc.rc, pc.state != 0 || c.Err() != nil)
	return err
}

// 底层Conn.Close(), 关闭net.conn
func (c *conn) Close() error {
	c.mu.Lock()
	c.ctx = nil
	err := c.err
	if c.err == nil {
		c.err = errors.New("redigo: closed")
		err = c.conn.Close()
	}
	c.mu.Unlock()
	return err
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值