【轉】dive into golang database/sql(2)

轉,原文: https://www.jianshu.com/p/807257fcb985?utm_campaign=studygolang.com&utm_medium=studygolang.com&utm_source=studygolang.com

 

----------

當我們拿到一個DB實例之後就可以操作數據庫了。本篇我們將更加深入database/sql包,共同探討連接池的維護請求的處理

上一篇我們一起學習了what on earth the DB object is。同時我畫了一張圖進行說明:

 
DB

上圖中很多部分在上一篇中都還沒有涉及到,因爲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
來源:簡書
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章