----------
當我們拿到一個DB
實例之後就可以操作數據庫了。本篇我們將更加深入database/sql
包,共同探討連接池的維護
和請求的處理
。
上一篇我們一起學習了what on earth the DB object is
。同時我畫了一張圖進行說明:
上圖中很多部分在上一篇中都還沒有涉及到,因爲
sql.Open
方法僅僅就是返回這樣一個DB
對象並新開一個goroutine connectionOpener通過監聽openerCh來新建連接。本章我們將更加全面更加深入地介紹DB對象,學習它是如何創建連接並維護連接池的。
從db.Query
說起
繼續那段最常見的代碼:
db,_ := sql.Open("mysql", "xxx")
rows,_ := db.Query("SELECT age,name from student where score > ?", 85)
defer rows.Close()
for rows.Next() {
var age int
var name string
_ = rows.Scan(&age, &name)
fmt.Println(age, name)
}
上面的代碼爲了簡便我忽略的所有的錯誤處理,但實際項目中你必須處理任何的錯誤!
當我們拿到db對象之後就可以進行Query了,那麼Query背後到底發生了什麼呢?源碼非常簡單,就只有幾行:
// Query executes a query that returns rows, typically a SELECT.
// The args are for any placeholder parameters in the query.
func (db *DB) Query(query string, args ...interface{}) (*Rows, error) {
var rows *Rows
var err error
for i := 0; i < maxBadConnRetries; i++ {
rows, err = db.query(query, args, cachedOrNewConn)
if err != driver.ErrBadConn {
break
}
}
if err == driver.ErrBadConn {
return db.query(query, args, alwaysNewConn)
}
return rows, err
}
其實這個Query方法只是做了一層簡單的包裝,僅從這裏我們依然看不出具體的行爲,但是我們能夠了解到的是,如果錯誤是driver.ErrBadConn
的話,sql包
默認幫我們做了maxBadConnRetries
次重試。
// maxBadConnRetries is the number of maximum retries if the driver returns
// driver.ErrBadConn to signal a broken connection before forcing a new
// connection to be opened.
const maxBadConnRetries = 2
那我們再繼續深入看看db.query
方法究竟做了哪些工作:
func (db *DB) query(query string, args []interface{}, strategy connReuseStrategy) (*Rows, error) {
ci, err := db.conn(strategy)
if err != nil {
return nil, err
}
return db.queryConn(ci, ci.releaseConn, query, args)
}
當然,細節也不明顯。不過不用急,一步一步來。可以發現db.query
做了兩件事情:
- 根據某種策略(strategy)獲取一個數據庫連接
- 基於這個連接進行query操作
其實,所有的數據庫操作都是這樣:
- 先獲取數據庫連接
- 基於此連接執行目標指令
接下來,我們將重點看看獲取數據庫連接
這部分的實現。
獲取數據庫連接
獲取數據庫連接的db.conn
方法稍微有點長(60行左右),這裏我給一個簡略的僞代碼版本:
func (db *DB) conn(strategy xxx) (*driverConn, error) {
lock()
defer unlock()
if strategy==cachedOrNewConn && anyFreeConnCanReuse(db.freeConn) {
conn := getOneConnFrom(db.freeConn)
maintain(db.freeConn)
return conn,nil
}
if db.maxOpen > 0 && db.numOpen >= db.maxOpen {
db.connRequests = append(db.connRequests, dontReleaseConnToFreeConnGiveIt2MeInstead)
ret := <- dontReleaseConnToFreeConnGiveIt2MeInstead
return ret.conn,nil
}
conn := openANewConn(db.driver)
maintainSomeInfo()
return conn,nil
}
從僞代碼裏可以看出,獲取一個數據庫連接分三種情況:
- 如果獲取策略是
cachedOrNewConn
,就從現有的連接池裏取一個空閒連接 - 如果連接池裏無可用連接,而連接數又已經到達配置的上限值,就發送一個
坐等連接
的通知,然後阻塞地在這裏等等待(其它地方釋放連接時會優先處理坐等連接
的通知請求) - 如果連接池無可用連接,而現有連接數還沒有達到配置的最大值,就通過driver再新建一個連接。
上面db.freeConn
其實就是一個[]*driverConn
,裏面存放了空閒的數據庫連接。
比較有意思的是第二點中的坐等連接
,怎麼個坐等法呢?看看實際代碼就明白了:
if db.maxOpen > 0 && db.numOpen >= db.maxOpen {
// Make the connRequest channel. It's buffered so that the
// connectionOpener doesn't block while waiting for the req to be read.
req := make(chan connRequest, 1)
db.connRequests = append(db.connRequests, req)
db.mu.Unlock()
ret, ok := <-req
if !ok {
return nil, errDBClosed
}
if ret.err == nil && ret.conn.expired(lifetime) {
ret.conn.Close()
return nil, driver.ErrBadConn
}
return ret.conn, ret.err
}
先看看connRequest
的定義:
// connRequest represents one request for a new connection
// When there are no idle connections available, DB.conn will create
// a new connRequest and put it on the db.connRequests list.
type connRequest struct {
conn *driverConn
err error
}
而db.connRequests
其實就是[]chan connRequest
所以坐等連接
其實就是,把一個connRequest放入db.connRequests
中,等待它被填充。當它被填充過了,於是我們就可以從它裏面拿到數據庫連接了。
“喂!db大哥!現在新建不了連接了,但是我急着要,你那兒有了空閒的就趕緊幫我放到connRequest
裏面,我在這兒等着呢”
那麼到底是什麼時候db會去填充這個connRequest
?猜猜看?
很容易想到,是在釋放連接的時候。每當一個連接使用完畢想要釋放時,通常會想到將它放入freeConn
隊列中。這時,可以先檢測connRequests
中有沒有坐等連接
的請求,有的話就可以把連接分給那個請求,而不是放進freeConn。這也符合freeConn的定義,既然有任務等着用連接,顯然freeConn裏是不應該有連接的。但到底是不是這樣的呢?一起看看代碼:
// Satisfy a connRequest or put the driverConn in the idle pool and return true
// or return false.
// putConnDBLocked will satisfy a connRequest if there is one, or it will
// return the *driverConn to the freeConn list if err == nil and the idle
// connection limit will not be exceeded.
// If err != nil, the value of dc is ignored.
// If err == nil, then dc must not equal nil.
// If a connRequest was fulfilled or the *driverConn was placed in the
// freeConn list, then true is returned, otherwise false is returned.
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 {
req := db.connRequests[0]
// This copy is O(n) but in practice faster than a linked list.
// TODO: consider compacting it down less often and
// moving the base instead?
copy(db.connRequests, db.connRequests[1:])
db.connRequests = db.connRequests[:c-1]
if err == nil {
dc.inUse = true
}
req <- connRequest{
conn: dc,
err: err,
}
return true
} else if err == nil && !db.closed && db.maxIdleConnsLocked() > len(db.freeConn) {
db.freeConn = append(db.freeConn, dc)
db.startCleanerLocked()
return true
}
return false
}
首先解釋一下方法名putConnDBLocked
。在sql包中,如果某個方法當且僅當會在加鎖的情況下被調用,那麼就會給這個方法加上Locked的後綴,方便開發者理解。
在putConnDBLocked
方法中,首先會去檢測db.connRequests
裏是否有坐等連接
的請求,如果有的話就用當前要釋放的連接去滿足那個請求。只有當發現沒有請求時,纔會把連接放到freeConn
中。
這有一個問題:
爲什麼不把所有的連接全部釋放到一個channel裏,任何需要連接的都通過
conn <- bufferedChan
這樣的方式統一來處理,而要選擇用freeConn和connRequests兩個slice來曲折地實現呢?
我覺得作者主要考慮的問題是公平性。如果多個goroutine同時在取某個channel,那麼當channel中新加一條消息時,無法確定這條消息被誰取走了,大家的機會都是均等的。在極端情況下,這可能出現某個等着獲取連接的請求永遠取不到連接。
使用connRequest對請求進行排隊,這樣可以讓先等待的一方在有連接可用時可以先用上。但是對於每次取隊首元素的場景,代碼實現爲什麼會選擇用slice而不是鏈表?
req := db.connRequests[0]
// This copy is O(n) but in practice faster than a linked list.
copy(db.connRequests, db.connRequests[1:])
db.connRequests = db.connRequests[:c-1]
代碼中有註釋說:
雖然copy是O(n)的複雜度,但是實際情況是比鏈表更快。
copy具體的實現由於在彙編代碼裏所以暫時沒有看,如果真的是不輸於鏈表的話,我猜測copy(s1, s2)執行的其實類似於
s1.Head = s2.Head
如果是這樣的話,那copy確實性能很好。
後續我會專門寫一篇文章來分析builtin copy
。
當獲取到數據庫連接之後,就可以基於這個連接進行真實的數據庫操作了。
下一章我們將一起探討真正的請求操作。
作者:suoga
鏈接:https://www.jianshu.com/p/807257fcb985
來源:簡書
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。