前言
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)
}
}