1.MySQL驅動註冊即連接池啓動
github.com/go-sql-driver/mysql/driver.go中的init方法實現mysql驅動註冊
func init() {
sql.Register("mysql", &MySQLDriver{})
}
上面的init方法實際是調用"database/sql/driver"基礎包中的Register()
方法,如下:
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 = make(map[string]driver.Driver)
// drivers是個map,註冊只是將“mysql”和一個空結構體MySQLDriver的指針存入map
drivers[name] = driver
}
在註冊驅動時不會真正連接數據庫,只有在調用Open()
方法的時候纔會連接,如下:
func Open(driverName, dataSourceName string) (*DB, error) {
driversMu.RLock()
// 獲取驅動結構體指針
driveri, ok := drivers[driverName]
driversMu.RUnlock()
if !ok {
return nil, fmt.Errorf("sql: unknown driver %q (forgotten import?)", driverName)
}
if driverCtx, ok := driveri.(driver.DriverContext); ok {
// 調用驅動中的OpenConnector
// func (d MySQLDriver) OpenConnector(dsn string) (driver.Connector, error) {
// cfg, err := ParseDSN(dsn)
// if err != nil {
// return nil, err
// }
// return &connector{
// cfg: cfg,
// }, nil
// }
// 實際是返回了dsn對應Config結構體指針
connector, err := driverCtx.OpenConnector(dataSourceName)
if err != nil {
return nil, err
}
//func OpenDB(c driver.Connector) *DB {
// ctx, cancel := context.WithCancel(context.Background())
// db := &DB{
// connector: c,
// openerCh: make(chan struct{}, connectionRequestQueueSize),
// 這裏默認chan的容量是50,高併發會不會產生大量的阻塞???
// resetterCh: make(chan *driverConn, 50),
// lastPut: make(map[*driverConn]string),
// connRequests: make(map[uint64]chan connRequest),
// stop: cancel,
// }
//
// 啓動創建連接gotoutine
// go db.connectionOpener(ctx)
// 啓動清理session的goroutine
// go db.connectionResetter(ctx)
//
// return db
//}
// 這裏的OpenDB方法是初始化DB結構體
return OpenDB(connector), nil
}
// 這一句是在轉換driver.DriverContext接口失敗時執行
// 因爲go1.10之前默認是一下方法,1.10後使用driver.DriverContext接口
// 下面是爲了做兼容
return OpenDB(dsnConnector{dsn: dataSourceName, driver: driveri}), nil
}
2.sql.DB結構體
爲什麼要介紹這個結構體,因爲連接池的主要實現就是基於sql.DB和sql.driverConn來實現的。
type DB struct {
// 是一個包含Connect(context.Context) (Conn, error)和Driver() Driver方法的接口。
// 這兩個接口是需要實際的數據庫驅動來實現,再使用;
connector driver.Connector
// 是自sql.DB創建後關閉過的連接數;
numClosed uint64
mu sync.Mutex
// 實際空閒連接數;
freeConn []*driverConn
// 存儲pending連接的map,當numOpen大於maxOpen時,連接會被暫存到該map中;
connRequests map[uint64]chan connRequest
// 即connRequests的key,記錄當前可用的最新的connRequest的key;
nextRequest uint64
// 當前活躍連接數和當前pending的連接數的總和
numOpen int
// 標記創建連接的的通知channel,獨立的goroutine異步消費該channel去執行創建;
openerCh chan struct{}
// 負責重置session的channel,獨立的goroutine異步消費該channel去執行重置;
resetterCh chan *driverConn
// 標記DB是否關閉;
closed bool
// 記錄db與conn之間的依賴關係,維持連接池以及關閉時使用;
dep map[finalCloser]depSet
// 最新入棧的連接,debug時使用,string中存的是棧buf相關信息;
lastPut map[*driverConn]string
// 最大空閒連接數;
maxIdle int
// 最大連接數;
maxOpen int
// 控線連接的最大存活時間;
maxLifetime time.Duration
// 標記超時連接的通知channel;
cleanerCh chan struct{}
// 通過context通知關閉connection opener和session resetter;
stop func()
}
3.申請連接過程
go從連接池中獲取連接時有兩個策略:
// 請求新的連接
alwaysNewConn connReuseStrategy = iota
// 從連接池中獲取連接
cachedOrNewConn
這裏以一個普通查詢過程爲例,看下連接池如何工作:
// 一般查詢代碼
rows, err := dbt.db.Query("SELECT * FROM test")
// Query如下,包了QueryContext方法
func (db *DB) Query(query string, args ...interface{}) (*Rows, error) {
// context.Background()是爲了創建一個根上下文,以便close的時候能夠將資源徹底的釋放
return db.QueryContext(context.Background(), query, args...)
}
//QueryContext方法
func (db *DB) QueryContext(ctx context.Context, query string, args ...interface{}) (*Rows, error) {
var rows *Rows
var err error
// maxBadConnRetries是個靜態變量爲2,這裏最多會執行兩次從連接池中獲取連接,如果在兩次獲取
// 過程中獲取到可用連接則直接返回
for i := 0; i < maxBadConnRetries; i++ {
rows, err = db.query(ctx, query, args, cachedOrNewConn)
if err != driver.ErrBadConn {
break
}
}
// 如果兩次都獲取不到可用連接,則以請求獲取一個新連接的方式獲取並返回
if err == driver.ErrBadConn {
return db.query(ctx, query, args, alwaysNewConn)
}
return rows, err
}
// query方法如下
func (db *DB) query(ctx context.Context, query string, args []interface{}, strategy connReuseStrategy) (*Rows, error) {
// 這裏是重點,這是真正申請連接的過程
dc, err := db.conn(ctx, strategy)
if err != nil {
return nil, err
}
// 這裏是實際的查詢過程,不過多介紹
return db.queryDC(ctx, nil, dc, dc.releaseConn, query, args)
}
真正根據策略申請連接的過程就在db.conn()
方法中:
func (db *DB) conn(ctx context.Context, strategy connReuseStrategy) (*driverConn, error) {
db.mu.Lock()
if db.closed {
db.mu.Unlock()
return nil, errDBClosed
}
// 判斷context是否超時,因爲ctx可以設置有超時時間的也可以設置無超時時間的
select {
default:
case <-ctx.Done():
db.mu.Unlock()
return nil, ctx.Err()
}
lifetime := db.maxLifetime
// 嘗試獲取一個空閒連接
numFree := len(db.freeConn)
if strategy == cachedOrNewConn && numFree > 0 {
// 取出第一個連接
conn := db.freeConn[0]
// copy是在原數組上覆蓋,所以需要最後尾數項
copy(db.freeConn, db.freeConn[1:])
db.freeConn = db.freeConn[:numFree-1]
// 標記conn在使用
conn.inUse = true
db.mu.Unlock()
// 如果conn達到超時時間,直接關閉連接,返回nil
if conn.expired(lifetime) {
conn.Close()
return nil, driver.ErrBadConn
}
// 加鎖確保conn已經reset完畢
conn.Lock()
err := conn.lastErr
conn.Unlock()
if err == driver.ErrBadConn {
conn.Close()
return nil, driver.ErrBadConn
}
return conn, nil
}
// 如果實際連接數已經達到最大連接數,則將新到的請求阻塞,並存到db.connRequests(上面提到的)
if db.maxOpen > 0 && db.numOpen >= db.maxOpen {
// 創建一個req chan作爲線連接的緩存,暫存到db.connRequests中
req := make(chan connRequest, 1)
// nextRequestKeyLocked()實際就是去取db.nextRequest作爲key
reqKey := db.nextRequestKeyLocked()
db.connRequests[reqKey] = req
db.mu.Unlock()
// context超時處理
select {
case <-ctx.Done():
// 如果超時保證新增的reqKey會被刪除掉
db.mu.Lock()
delete(db.connRequests, reqKey)
db.mu.Unlock()
select {
default:
case ret, ok := <-req:
if ok {
db.putConn(ret.conn, ret.err, false)
}
}
return nil, ctx.Err()
// 當req真正接到寫入的時候觸發下面的操作,其實和上面的邏輯類似
case ret, ok := <-req:
if !ok {
return nil, errDBClosed
}
if ret.err == nil && ret.conn.expired(lifetime) {
ret.conn.Close()
return nil, driver.ErrBadConn
}
if ret.conn == nil {
return nil, ret.err
}
// 加鎖確保conn已經reset完畢
ret.conn.Lock()
err := ret.conn.lastErr
ret.conn.Unlock()
if err == driver.ErrBadConn {
ret.conn.Close()
return nil, driver.ErrBadConn
}
return ret.conn, ret.err
}
}
// 如果連接數沒有大於最大連接數時,進入以下邏輯
// 先把當點連接數加1
db.numOpen++
db.mu.Unlock()
// 獲取一個新連接,這裏也就是alwaysNewConn策略的實現
ci, err := db.connector.Connect(ctx)
if err != nil {
db.mu.Lock()
// 錯誤就回滾numOpen
db.numOpen--
// 很重要!!!下面說
db.maybeOpenNewConnections()
db.mu.Unlock()
return nil, err
}
db.mu.Lock()
// 構造成driverConn返回
dc := &driverConn{
db: db,
createdAt: nowFunc(),
ci: ci,
inUse: true,
}
// debug時會用到這個方法
db.addDepLocked(dc, dc)
db.mu.Unlock()
return dc, nil
}
在這個方法的68行的case ret, ok := <-req:這部分,有人會很困惑,這個req是個空的chan,怎麼實行case裏面的代碼?connRequests
中存的明明是個空的conn,之後怎麼用呢?答案在以下方法裏:
func (db *DB) maybeOpenNewConnections() {
// 當和maxOpen比較還可以繼續創建連接時
// 如果阻塞的連接過多,那麼也只能建立db.maxOpen - db.numOpen個
numRequests := len(db.connRequests)
if db.maxOpen > 0 {
numCanOpen := db.maxOpen - db.numOpen
if numRequests > numCanOpen {
numRequests = numCanOpen
}
}
// 循環numRequests次,給openerCh添加佔位量,異步goroutine接收到即可創建新的連接
for numRequests > 0 {
db.numOpen++ // optimistically
numRequests--
if db.closed {
return
}
db.openerCh <- struct{}{}
}
}
可能有人還是很蒙,這和connRequests
這個map還是沒關係啊,這裏就關係到db.openerCh這個創建相關的chan如果處理了:
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()
}
db.numOpen--
return
}
if err != nil {
db.numOpen--
// 關鍵點!!!
db.putConnDBLocked(nil, err)
// 這個就是嘗試db.openerCh <- struct{}{}這個操作
db.maybeOpenNewConnections()
return
}
dc := &driverConn{
db: db,
createdAt: nowFunc(),
ci: ci,
}
// 關鍵點!!!創建好的dc先傳入了這個方法
if db.putConnDBLocked(dc, err) {
db.addDepLocked(dc, dc)
} else {
db.numOpen--
ci.Close()
}
}
看下db.putConnDBLocked(nil, err)這個方法:
func (db *DB) putConnDBLocked(dc *driverConn, err error) bool {
if db.closed {
return false
}
if db.maxOpen > 0 && db.numOpen > db.maxOpen {
return false
}
// 這裏判斷了db.connRequests是否大於0,即是否有阻塞請求
if c := len(db.connRequests); c > 0 {
var req chan connRequest
var reqKey uint64
// 有阻塞請求,先delete掉一個key
for reqKey, req = range db.connRequests {
break
}
delete(db.connRequests, reqKey)
if err == nil {
dc.inUse = true
}
// 將剛申請好的dc包裝成connRequest賦給req
// 這裏的req就是range db.connRequests返回的req
// 這裏真正的獲取到了連接!!!
req <- connRequest{
conn: dc,
err: err,
}
return true
} else if err == nil && !db.closed && db.maxIdleConnsLocked() > len(db.freeConn) {
// 如果沒有阻塞的請求,將新建連接存入db.freeConn空閒連接池,並執行一次清理連接
db.freeConn = append(db.freeConn, dc)
db.startCleanerLocked()
return true
}
return false
}
這就是整個連接池的運行原理。
附.go基礎包的數據庫連接池存在的問題
1.連接在用完會被清理session歸還連接池,如果當前連接數大於maxIdle
,該連接會被專門的goroutine直接close掉,沒有延遲時間。如果配置不合理可能會造成連接不停的創建和銷燬。