底层数据结构
连接池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()
获取连接这里有几个优先级:
- 先从空闲连接队列p.freeItem中获取,取到直接返回连接
- freeItem中没有,要看此时活跃连接数是否已达上线,未达上线则新建一个连接
- 注意:freeItem中的空闲连接也在活跃连接p.active的统计中,即freeItem中的连接也是活跃连接
- 如果活跃连接数已达到上线,此时需要用到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
}