案例
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é)果如下圖所示

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é)果如下圖所示

可以看出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)邏輯如下圖所示

案例分析
有了前面的背景知識,我們來分析下上面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)連接泄露的問題。