Go組件學習——database/sql數據庫連接池你用對了嗎


1、案例

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設置為10,足夠這里的兩次查詢使用了。

程序正常執行並結束,打印了一堆沒有處理的結果,如下:

&{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設置為1,但是這里有兩次查詢,需要兩個連接,通過調試發現一直阻塞在

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

之所以阻塞,是因為拿不到連接,可用的連接一直被上一次查詢占用了。

 

執行結果如下圖所示

 

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

通過case2發現可能會存在連接泄露的情況,所以繼續保持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遍歷的代碼。

 

執行結果如下

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]]}

  

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

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

 

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的遍歷代碼,可以讓下一次查詢拿到連接,那我們繼續考察,如果在rows遍歷的過程中發生了以外提前退出了,是否影響后面sql語句的執行。

 

執行結果如下圖所示

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

 

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是不是就沒救了,只能一直阻塞在第二次查詢了?

看上面的代碼,在異常退出后,我們調用了關閉rows的語句,繼續執行第二次查詢。

 

執行結果如下

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]]}

這次,從執行結果看,第二次查詢正常執行,並沒有阻塞。

 

所以,這是為什么呢?

下面先看看database/sql的連接池是如何實現的

 

2、database/sql的連接池

網上關於database/sql連接池的實現有很多介紹文章。

其中gorm這樣的orm框架的數據庫連接池也是復用database/sql的連接池。

大致分為四步

第一步:驅動注冊

我們提供下上面幾個case所在的main函數代碼

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()
}

這里說的驅動注冊就是指

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

也可以使用gorm中的MySQL驅動注冊即

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

驅動注冊主要是注冊不同的數據源,比如MySQL、PostgreSQL等

 

第二步:初始化DB

初始化DB即調用Open函數,這時候其實沒有真的去獲取DB操作的連接,只是初始化得到一個DB的數據結構。

 

第三步:獲取連接

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

以Query方法為例,可以一直追蹤源碼實現,源碼實現路徑如下

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

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

如果當前實際連接數已經超過最大連接數即上面case中提到的maxOpenConns,則將任務添加到任務隊列中等待;

以上情況都不滿足,則自行創建一個新的連接用於執行DB操作。

 

第四步:釋放連接

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

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

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

 

連接池的完整實現邏輯如下圖所示

 

3、案例分析

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

case1

最大連接數為10個,代碼中只有兩個查詢任務,完全可以創建兩個連接執行。

 

case2

最大連接數為1個,第一次查詢已經占用。第二次查詢之所以阻塞是因為第一次查詢完成后沒有釋放連接,又因為最大連接數只能是1的限制,導致第二次查詢拿不到連接。

 

case3

最大連接數為1個,但是在第一次查詢完成后,調用了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遍歷會在最后一次遍歷的時候調用rows.Close()方法,該方法會釋放連接。

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

 

case4

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

case3中已經說明過,在最后一次遍歷才會調用rows.Close()方法,因為這里的rows遍歷中途退出了,導致釋放連接的代碼沒有執行到。所以第二次查詢依然阻塞,拿不到連接。

 

case5

最大連接數為1個,使用了rows遍歷,且中途以外退出,但是主動調用了rows.Close(),等價於rows遍歷完整執行,即釋放了連接,所以第二次查詢拿到連接正常執行查詢任務。

注意:在實際開發中,我們更多使用的是下面的優雅方式

defer rows.Close()

  

4、心得體會

最近本來是在看gorm的源碼,也想過把gorm應用到我們的項目組里,但是因為一些二次開發以及性能問題,上馬gorm的計划先擱置了。

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

在看這塊代碼的時候,還發現了我們項目的部分代碼中使用了rows遍歷,但是忘記添加defer rows.Close()的情況。這種情況一般不會有什么問題,但是如果因為一些意外情況導致提前退出遍歷,則可能會出現連接泄露的問題。

 

如果您覺得閱讀本文對您有幫助,請點一下“推薦”按鈕,您的“推薦”將是我最大的寫作動力!如果您想持續關注我的文章,請掃描二維碼,關注JackieZheng的微信公眾號,我會將我的文章推送給您,並和您一起分享我日常閱讀過的優質文章。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM