Go組件學(xué)習(xí)——database/sql數(shù)據(jù)庫連接池你用對了嗎

案例

case1: maxOpenConns > 1

func fewConns() {
    db, _ := db.Open("mysql", "root:rootroot@/dqm?charset=utf8&parseTime=True&loc=Local")

    db.SetMaxOpenConns(10)
    rows, err := db.Query("select * from test where name = 'jackie' limit 10")
    if err != nil {
        fmt.Println("query error")
    }

    row, _ := db.Query("select * from test") 
    fmt.Println(row, rows)
}

這里maxOpenConns設(shè)置為10,足夠這里的兩次查詢使用了。

程序正常執(zhí)行并結(jié)束,打印了一堆沒有處理的結(jié)果,如下:

&{0xc0000fc180 0x10bbb80 0xc000106050 <nil> <nil> {{0 0} 0 0 0 0} false <nil> []} &{0xc0000f4000 0x10bbb80 0xc0000f8000 <nil> <nil> {{0 0} 0 0 0 0} false <nil> []}

case2: maxOpenConns = 1

func oneConn() {
    db, _ := db.Open("mysql", "root:rootroot@/dqm?charset=utf8&parseTime=True&loc=Local")

    db.SetMaxOpenConns(1)
    rows, err := db.Query("select * from test where name = 'jackie' limit 10")
    if err != nil {
        fmt.Println("query error")
    }

    row, _ := db.Query("select * from test")
    fmt.Println(row, rows)
}

這里maxOpenConns設(shè)置為1,但是這里有兩次查詢,需要兩個(gè)連接,通過調(diào)試發(fā)現(xiàn)一直阻塞在

row, _ := db.Query("select * from test")

之所以阻塞,是因?yàn)槟貌坏竭B接,可用的連接一直被上一次查詢占用了。

執(zhí)行結(jié)果如下圖所示

image.png

case3: maxOpenConns = 1 + for rows.Next()

通過case2發(fā)現(xiàn)可能會存在連接泄露的情況,所以繼續(xù)保持maxOpenConns=1

func oneConnWithRowsNext() {
    db, _ := db.Open("mysql", "root:rootroot@/dqm?charset=utf8&parseTime=True&loc=Local")

    db.SetMaxOpenConns(1)
    rows, err := db.Query("select * from test where name = 'jackie' limit 10")
    if err != nil {
        fmt.Println("query error")
    }

    for rows.Next() {
        fmt.Println("close")
    }

    row, _ := db.Query("select * from test")
    fmt.Println(row, rows)
}

除了maxOpenConns=1以外,這里多了rows遍歷的代碼。

執(zhí)行結(jié)果如下

close
close
close
close
close
close
&{0xc000104000 0x10bbfe0 0xc0000e40f0 <nil> <nil> {{0 0} 0 0 0 0} false <nil> []} &{0xc000104000 0x10bbfe0 0xc0000e40a0 <nil> <nil> {{0 0} 0 0 0 0} true 0xc00008e050 [[97 99] [105 101 2 49 56 12] [0 12]]}

顯然,這里第二次查詢并沒有阻塞,而是拿到了連接并查到了結(jié)果。

所以,這里rows遍歷一定幫我們做了一些有關(guān)獲取連接的事情,后面展開。

case4: maxOpenConns = 1 + for rows.Next() + 異常退出

func oneConnWithRowsNextWithError() {
    db, _ := db.Open("mysql", "root:rootroot@/dqm?charset=utf8&parseTime=True&loc=Local")

    db.SetMaxOpenConns(1)
    rows, err := db.Query("select * from test where name = 'jackie' limit 10")
    if err != nil {
        fmt.Println("query error")
    }

    i := 1
    for rows.Next() {
        i++
        if i == 3 {
            break
        }
        fmt.Println("close")
    }

    row, _ := db.Query("select * from test")
    fmt.Println(row, rows)
}

case3中添加了rows的遍歷代碼,可以讓下一次查詢拿到連接,那我們繼續(xù)考察,如果在rows遍歷的過程中發(fā)生了以外提前退出了,是否影響后面sql語句的執(zhí)行。

執(zhí)行結(jié)果如下圖所示

image.png

可以看出rows遍歷的提前結(jié)束,影響了后面查詢,出現(xiàn)了和case2同樣的情況,即拿不到數(shù)據(jù)庫連接,一直阻塞。

case5: maxOpenConns = 1 + for rows.Next() + 異常退出 + rows.Close()

func oneConnWithRowsNextWithErrorWithRowsClose() {
    db, _ := db.Open("mysql", "root:rootroot@/dqm?charset=utf8&parseTime=True&loc=Local")

    db.SetMaxOpenConns(1)
    rows, err := db.Query("select * from test where name = 'jackie' limit 10")
    if err != nil {
        fmt.Println("query error")
    }

    i := 1
    for rows.Next() {
        i++
        if i == 3 {
            break
        }
        fmt.Println("close")
    }
    rows.Close()


    row, _ := db.Query("select * from test")
    fmt.Println(row, rows)
}

case4是不是就沒救了,只能一直阻塞在第二次查詢了?

看上面的代碼,在異常退出后,我們調(diào)用了關(guān)閉rows的語句,繼續(xù)執(zhí)行第二次查詢。

執(zhí)行結(jié)果如下

close
&{0xc00010c000 0x10f0ab0 0xc0000e80a0 <nil> <nil> {{0 0} 0 0 0 0} false <nil> []} &{0xc00010c000 0x10f0ab0 0xc0000e8050 <nil> <nil> {{0 0} 0 0 0 0} true <nil> [[51] [104 101 108 108 111 2] [56 11]]}

這次,從執(zhí)行結(jié)果看,第二次查詢正常執(zhí)行,并沒有阻塞。

所以,這是為什么呢?

下面先看看database/sql的連接池是如何實(shí)現(xiàn)的

database/sql的連接池

網(wǎng)上關(guān)于database/sql連接池的實(shí)現(xiàn)有很多介紹文章。

其中g(shù)orm這樣的orm框架的數(shù)據(jù)庫連接池也是復(fù)用database/sql的連接池。

大致分為四步

第一步:驅(qū)動注冊

我們提供下上面幾個(gè)case所在的main函數(shù)代碼

package main

import (
    db "database/sql"
    "fmt"
    //_ "github.com/jinzhu/gorm/dialects/mysql"
    _ "github.com/go-sql-driver/mysql"
)

func main() {
    // maxConn > 1
    fewConns()
    // maxConn = 1
    oneConn()

    // maxConn = 1 + for rows.Next()
    oneConnWithRowsNext()
    // maxConn = 1 + for rows.Next() + 提前退出
    oneConnWithRowsNextWithError()
    // maxConn = 1 + for rows.Next() + 提前退出 + defer rows.Close()
    oneConnWithRowsNextWithErrorWithRowsClose()
}

這里說的驅(qū)動注冊就是指

    _ "github.com/go-sql-driver/mysql"

也可以使用gorm中的MySQL驅(qū)動注冊即

_ "github.com/jinzhu/gorm/dialects/mysql"

驅(qū)動注冊主要是注冊不同的數(shù)據(jù)源,比如MySQL、PostgreSQL等

第二步:初始化DB

初始化DB即調(diào)用Open函數(shù),這時(shí)候其實(shí)沒有真的去獲取DB操作的連接,只是初始化得到一個(gè)DB的數(shù)據(jù)結(jié)構(gòu)。

第三步:獲取連接

獲取連接是在具體的sql語句中執(zhí)行的,比如Query方法、Exec方法等。

以Query方法為例,可以一直追蹤源碼實(shí)現(xiàn),源碼實(shí)現(xiàn)路徑如下

sql.go(Query()) -> sql.go(QueryContext()) -> sql.go(query()) -> sql.go(conn())

進(jìn)入conn()方法的具體實(shí)現(xiàn)邏輯是如果連接池中有空閑的連接且沒有過期的就直接拿出來用;

如果當(dāng)前實(shí)際連接數(shù)已經(jīng)超過最大連接數(shù)即上面case中提到的maxOpenConns,則將任務(wù)添加到任務(wù)隊(duì)列中等待;

以上情況都不滿足,則自行創(chuàng)建一個(gè)新的連接用于執(zhí)行DB操作。

第四步:釋放連接

當(dāng)DB操作結(jié)束后,需要將連接釋放,比如放回到連接池中,以便下一次DB操作的使用。

釋放連接的代碼實(shí)現(xiàn)在sql.go中的putConn()方法。

其主要做的工作是判定連接是否過期,如果沒有過期則放回連接池。

連接池的完整實(shí)現(xiàn)邏輯如下圖所示

image.png

案例分析

有了前面的背景知識,我們來分析下上面5個(gè)case

case1

最大連接數(shù)為10個(gè),代碼中只有兩個(gè)查詢?nèi)蝿?wù),完全可以創(chuàng)建兩個(gè)連接執(zhí)行。

case2

最大連接數(shù)為1個(gè),第一次查詢已經(jīng)占用。第二次查詢之所以阻塞是因?yàn)榈谝淮尾樵兺瓿珊鬀]有釋放連接,又因?yàn)樽畲筮B接數(shù)只能是1的限制,導(dǎo)致第二次查詢拿不到連接。

case3

最大連接數(shù)為1個(gè),但是在第一次查詢完成后,調(diào)用了rows遍歷代碼。通過源碼可以知道rows遍歷代碼

func (rs *Rows) Next() bool {
    var doClose, ok bool
    withLock(rs.closemu.RLocker(), func() {
        doClose, ok = rs.nextLocked()
    })
    if doClose {
        rs.Close()
    }
    return ok
}

rows遍歷會在最后一次遍歷的時(shí)候調(diào)用rows.Close()方法,該方法會釋放連接。

所以case3的鏈接是在rows遍歷中釋放的

case4

最大連接數(shù)為1個(gè),也用了rows遍歷,但是連接仍然沒有釋放。

case3中已經(jīng)說明過,在最后一次遍歷才會調(diào)用rows.Close()方法,因?yàn)檫@里的rows遍歷中途退出了,導(dǎo)致釋放連接的代碼沒有執(zhí)行到。所以第二次查詢依然阻塞,拿不到連接。

case5

最大連接數(shù)為1個(gè),使用了rows遍歷,且中途以外退出,但是主動調(diào)用了rows.Close(),等價(jià)于rows遍歷完整執(zhí)行,即釋放了連接,所以第二次查詢拿到連接正常執(zhí)行查詢?nèi)蝿?wù)。

注意:在實(shí)際開發(fā)中,我們更多使用的是下面的優(yōu)雅方式

defer rows.Close()

心得體會

最近本來是在看gorm的源碼,也想過把gorm應(yīng)用到我們的項(xiàng)目組里,但是因?yàn)橐恍┒伍_發(fā)以及性能問題,上馬gorm的計(jì)劃先擱置了。

然后在看到gorm代碼的時(shí)候發(fā)現(xiàn)很多地方還是直接使用了database/sql,尤其是連接池這塊的實(shí)現(xiàn)。

在看這塊代碼的時(shí)候,還發(fā)現(xiàn)了我們項(xiàng)目的部分代碼中使用了rows遍歷,但是忘記添加defer rows.Close()的情況。這種情況一般不會有什么問題,但是如果因?yàn)橐恍┮馔馇闆r導(dǎo)致提前退出遍歷,則可能會出現(xiàn)連接泄露的問題。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容