1. 程式人生 > >Go元件學習——database/sql資料庫連線池你用對了嗎

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的微信公眾號,我會將我的文章推送給您,並和您一起分享我日常閱讀過的優質文章。