database/sql库连接池实现方法解读(原创)

前言

database/sql对外实现了驱动的接口,对内提供了调用底层驱动的方法,其灵活,巧妙的设计实现了底层驱动和业务之间的解耦,其方案被众多开源框架参考,比如beego的Cache库,Log库都是参考了其设计,堪称教科书。其连接池的实现方案同样巧妙,虽然繁杂,但思路和逻辑清晰,作为golang的学习者,本文着重分析连接池的实现。

连接池实现拓扑图:

在这里插入图片描述

1,驱动注册

drivers   = make(map[string]driver.Driver)

func Register(name string, driver driver.Driver) {
	driversMu.Lock()
	defer driversMu.Unlock()
	if driver == nil {
		panic("sql: Register driver is nil")
	}
	if _, dup := drivers[name]; dup {
		panic("sql: Register called twice for driver " + name)
	}
	drivers[name] = driver

}

这是database/sql库对外提供的注册函数,只要底层的驱动,例如mysql,实现了Driver的interface

type Driver interface {
    Open(name string) (Conn, error)
}

在业务中可通过import的时候执行init函数,自动注册:

import (
    "database/sql"
    _ "github.com/go-sql-driver/mysql"//自动执行init()函数
)    

mysql的init函数:

func init() {
    sql.Register("mysql", &MySQLDriver{})
}

2,打开DB句柄

业务中要执行sql语言,必须先打开一个DB的句柄,DB结构体如下:

type DB struct {
   connector driver.Connector// 用于获取driver.Conn 可以由驱动层实现,否则用sql.dsnConnector

   numClosed uint64 // 是一个原子计数器,代表总的关闭连接数量

   mu           sync.Mutex
   freeConn     []*driverConn //空闲连接池
   connRequests map[uint64]chan connRequest // 无可用连接时,处于 Pending 状态的连接请求
   nextRequest  uint64
   numOpen      int    // 打开和准备打开的连接总数

   openerCh    chan struct{} // 用来传信号的管道 表示需要多少新连接
   resetterCh  chan *driverConn // 用来传需要重置 Session 的 driverConn
   closed      bool 
   dep         map[finalCloser]depSet // 依赖记录
   lastPut     map[*driverConn]string 
   maxIdle     int                 
   maxOpen     int  
   maxLifetime time.Duration // 连接的生命后期
   cleanerCh   chan struct{} // 传信号 表示需要清理freeConn空闲池中已经关掉的driverConn

   stop func()
}

打开DB结构体句柄采用以下方法:

func main() {
    db, err := sql.Open("mysql","user:password@tcp(127.0.0.1:3306)/hello")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()
}

sql.Open()最总是调用OpenDB(c driver.Connector) *DB 得到DB结构体的指针:

func Open(driverName, dataSourceName string) (*DB, error) {
	driversMu.RLock()
	driveri, ok := drivers[driverName]//获得已经注册过的driver
	driversMu.RUnlock()
    .......
    //先判断是否实现了driver.DriverContext的接口
	if driverCtx, ok := driveri.(driver.DriverContext); ok {
		connector, err := driverCtx.OpenConnector(dataSourceName)//得到mysql的connecter
		if err != nil {
			return nil, err
		}
		return OpenDB(connector), nil//最后通过connector参数调用OpenDB
	}

    return OpenDB(dsnConnector{dsn: dataSourceName, driver: driveri}), nil
}

func OpenDB(c driver.Connector) *DB {
	ctx, cancel := context.WithCancel(context.Background())
	db := &DB{
		connector:    c,
		openerCh:     make(chan struct{}, connectionRequestQueueSize),
		resetterCh:   make(chan *driverConn, 50),
		lastPut:      make(map[*driverConn]string),
		connRequests: make(map[uint64]chan connRequest),
		stop:         cancel,
	}

	go db.connectionOpener(ctx) //connOpener 运行在一个单独的goroutine中
	go db.connectionResetter(ctx)//connResetter单独运行在一个goroutine中

	return db
}

这里值得注意的是,先判断底层的驱动是否实现了driver.DriverContext的接口,如果没有实现,会默认调用sql自己实现的dsnConnector,两者是有区别的,前者有Context的使用权,后者没有使用权。

3,获取连接

拿到DB结构体的指针后,可以调用sql.DB中任何的公有方法了,上面的OpenDB步骤并不会打开连接,真正打开连接是在执行query()前,分析一下DB.conn方法

    func (db *DB) conn(ctx context.Context, strategy connReuseStrategy) (*driverConn, error) {
     ......

	// 如果从连接池取连接
	numFree := len(db.freeConn)
	if strategy == cachedOrNewConn && numFree > 0 {
		conn := db.freeConn[0]
		copy(db.freeConn, db.freeConn[1:])
		db.freeConn = db.freeConn[:numFree-1]
		conn.inUse = true
		db.mu.Unlock()
		.......
		// 这里要注意,从连接池里拿到的有可能是还没重置会话的连接,所以要多一层lastErr的判断
		conn.Lock()
		err := conn.lastErr
		conn.Unlock()
		if err == driver.ErrBadConn {
			conn.Close()
			return nil, driver.ErrBadConn
		}
		return conn, nil
	}

	// 如果连接池没有空闲的连接,则生成一个连接请求到db.connRequestszh中,并且阻塞等待连接的到来
	if db.maxOpen > 0 && db.numOpen >= db.maxOpen {
		req := make(chan connRequest, 1)
		reqKey := db.nextRequestKeyLocked()
		db.connRequests[reqKey] = req
		db.waitCount++
		db.mu.Unlock()

		waitStart := time.Now()

		// Timeout the connection request with the context.
		select {
		case <-ctx.Done():
			........
			return nil, ctx.Err()
		case ret, ok := <-req:
	        atomic.AddInt64(&db.waitDuration, int64(time.Since(waitStart)))
	    	........
			ret.conn.Lock()
			// 这里要注意,从连接池里拿到的有可能是还没重置会话的连接,所以要多一层lastErr的判断
			err := ret.conn.lastErr
			ret.conn.Unlock()
			if err == driver.ErrBadConn {
				ret.conn.Close()
				return nil, driver.ErrBadConn
			}
			return ret.conn, ret.err
		}
	}

	db.numOpen++ // 先乐观地加一
	db.mu.Unlock()
	ci, err := db.connector.Connect(ctx)
	if err != nil {
		db.mu.Lock()
		db.numOpen-- // 如果出现错误再减一
		db.maybeOpenNewConnections()//请求异步建立请求
		db.mu.Unlock()
		return nil, err
	}
	db.mu.Lock()
	dc := &driverConn{
		db:        db,
		createdAt: nowFunc(),
		ci:        ci,
		inUse:     true,
	}
	db.addDepLocked(dc, dc)
	db.mu.Unlock()
	return dc, nil
}

db.conn 请求连接的方法有几点需要注意:

1)这里可能会觉得很繁琐,首先有多处检查ctx是否超时,我认为应该是加锁的原因,不同的goroutine获取锁有先后顺序,如果在等待的过程中连接超时,那就要先select处理超时,返回错误。

2)从空闲的连接池获取连接的方法一开始觉得有点奇怪,我个人觉得应该是这样处理的:

conn := db.freeConn[0]
db.freeConn = db.freeConn[1:]
直接获取第一元素

但database/sql做了这样的处理:

conn := db.freeConn[0]
copy(db.freeConn, db.freeConn[1:])
db.freeConn = db.freeConn[:numFree-1]

多了两步操作,先copy,再截取。我认为是这样的:slice本质是底层slice的view,包含了三个元素:底层数组指针,长度,容量。 第一种方式也可以实现,但会破坏数组的容量,即容量减少一个,但第二种方式的不会破坏数组的容量,把数组元素相当于向前移动一个位置,避免了append时再次分配内存。

3)有两次获取conn.lastErr的错误,是因为连接放回连接池过程中,会发送db.resetterCh,而这个重置会话的chan只有50个buffer(见OpenDB时make(chan *driverConn, 50)),所以有可能会重现阻塞的情况,当出现阻塞时,会标志这个连接为driver.ErrBadConn,接下来也会有分析。

4)当重新建立连接(没有空闲的连接或没有连接池数量配置时),如果出现err,则会再调用db.maybeOpenNewConnections()异步创建一个连接。看一下代码:

func (db *DB) maybeOpenNewConnections() {
	numRequests := len(db.connRequests)
	.....
	for numRequests > 0 {
		db.numOpen++ // 乐观的加一
		numRequests--
		if db.closed {
			return
		}
		db.openerCh <- struct{}{}//发送到openerCh
	}
}

connectionOpener运行在独立的goroutine中,如果cancle就退出,如果有新的连接创建请求,则调用db.openNewConnection(ctx)处理。

func (db *DB) connectionOpener(ctx context.Context) {
	for {
		select {
		case <-ctx.Done():
			return
		case <-db.openerCh:
			db.openNewConnection(ctx)
		}
	}
}

db.openNewConnection有两点要注意的:

1,发现有两处db.numOpen–,因为在maybeOpenNewConnections 已经乐观地加一,所以要减掉

2,把创建好的连接放入连接池中db.putConnDBLocked(dc, err)

    func (db *DB) openNewConnection(ctx context.Context) {
	ci, err := db.connector.Connect(ctx)
	db.mu.Lock()
	defer db.mu.Unlock()
	if db.closed {
		if err == nil {
			ci.Close()
		}
		//因为在maybeOpenNewConnections 已经乐观地加一,所以要减掉
		db.numOpen--
		return
	}
	if err != nil {
		db.numOpen--
		db.putConnDBLocked(nil, err)
		db.maybeOpenNewConnections()
		return
	}
	dc := &driverConn{
		db:        db,
		createdAt: nowFunc(),
		ci:        ci,
	}
	if db.putConnDBLocked(dc, err) {
		db.addDepLocked(dc, dc)
	} else {
		db.numOpen--
		ci.Close()
	}
}

4,连接回收到连接池

1)首先遍历dc.onPut,执行fn()

2)如果发现该连接不可用,则调用maybeOpenNewConnections() 异步创建一个连接,并且关闭不可用的连接。

3)如果连接成功被连接池回收,但db.resetterCh 阻塞了,则先标记连接为ErrBadConn,所以前面从连接池获取连接时每一次都会判断连接是否可用。

4)如果连接池满了,没回放成功,则会关闭该连接。

func (db *DB) putConn(dc *driverConn, err error, resetSession bool) {
	......
	for _, fn := range dc.onPut {
		fn()
	}
	dc.onPut = nil

	if err == driver.ErrBadConn {
		db.maybeOpenNewConnections()//异步创建连接
		db.mu.Unlock()
		dc.Close()
		return
	}
	if putConnHook != nil {
		putConnHook(db, dc)
	}
	if db.closed {
		resetSession = false
	}
	if resetSession {
		if _, resetSession = dc.ci.(driver.SessionResetter); resetSession {
            //先锁住
			dc.Lock()
		}
	}
	added := db.putConnDBLocked(dc, nil)
	db.mu.Unlock()

	if !added {
		if resetSession {
			dc.Unlock()
		}
		dc.Close()
		return
	}
	if !resetSession {
		return
	}
	select {
	default:
		//如果db.resetterCh 阻塞了,则先标记连接为ErrBadConn
		dc.lastErr = driver.ErrBadConn
		dc.Unlock()
	case db.resetterCh <- dc:
	}
}

调用putConnDBLocked(dc *driverConn, err error) bool处理,凡是以Locked结尾的方法,说明调用处要加锁,这更方便人理解。

1)查看有没有等待的请求队列len(db.connRequests),如果有,则把该连接直接通过chan发送给等待方,并在map中删除对应的key。

2)如果map中没有等待的连接请求,则追加到freeConn连接池append(db.freeConn, dc)。

3)调用startCleanerLocked()处理过期的连接,只会调用一次。

func (db *DB) putConnDBLocked(dc *driverConn, err error) bool {
	if db.closed {
		return false
	}
	if db.maxOpen > 0 && db.numOpen > db.maxOpen {
		return false
	}
	if c := len(db.connRequests); c > 0 {
		var req chan connRequest
		var reqKey uint64
		for reqKey, req = range db.connRequests {
			break
		}
		delete(db.connRequests, reqKey) //从等待队列中删除
		if err == nil {
			dc.inUse = true
		}
		req <- connRequest{
			conn: dc,
			err:  err,
		}
		return true
	} else if err == nil && !db.closed {
		if db.maxIdleConnsLocked() > len(db.freeConn) {
			db.freeConn = append(db.freeConn, dc)
			db.startCleanerLocked()
			return true
		}
		db.maxIdleClosed++
	}
	return false
}

5,处理过期的连接

1)开定时器,每隔一段时间检测空闲连接池中的连接是否过期

2)如果接收到db.cleanerCh的信号,也会遍历处理超时,db.cleanerCh的buffer只有1,一般在SetConnMaxLifetime检测生命周期配置变短时发送。

3)为了遍历空闲队列里面连接的公平性,做了一个巧妙的处理,一旦发现队列前面的连接过期,则会把最后一个连接放到最前面,然后从当前开始遍历。

4)遍历空闲队列发现超时的连接,把超时连接一个一个追加到关闭队列中append(closing, c),然后遍历关闭的队列,一个一个关闭。

func (db *DB) connectionCleaner(d time.Duration) {
	const minInterval = time.Second

	if d < minInterval {
		d = minInterval
	}
	t := time.NewTimer(d)

	for {
		select {
		case <-t.C:
		case <-db.cleanerCh: // maxLifetime was changed or db was closed.
		}

		db.mu.Lock()
		d = db.maxLifetime
		if db.closed || db.numOpen == 0 || d <= 0 {
			db.cleanerCh = nil
			db.mu.Unlock()
			return
		}

		expiredSince := nowFunc().Add(-d)
		var closing []*driverConn
		for i := 0; i < len(db.freeConn); i++ {
			c := db.freeConn[i]
			if c.createdAt.Before(expiredSince) {
				closing = append(closing, c)
				last := len(db.freeConn) - 1
				db.freeConn[i] = db.freeConn[last]
				db.freeConn[last] = nil
				db.freeConn = db.freeConn[:last]
				i--
			}
		}
		db.maxLifetimeClosed += int64(len(closing))
		db.mu.Unlock()

		for _, c := range closing {
			c.Close()
		}

		if d < minInterval {
			d = minInterval
		}
		t.Reset(d)
	}
}
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值