dive into golang database/sql(2)

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

當我們拿到一個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

當擷取到資料庫連接之後,就可以基於這個串連進行真實的資料庫操作了。

下一章我們將一起探討真正的請求操作。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.