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

案例

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,足夠這裏的兩次查詢使用了。mysql

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

&{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,可是這裏有兩次查詢,須要兩個鏈接,經過調試發現一直阻塞在github

row, _ := db.Query("select * from test")
複製代碼

之因此阻塞,是由於拿不到鏈接,可用的鏈接一直被上一次查詢佔用了。sql

執行結果以下圖所示數據庫

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

經過case2發現可能會存在鏈接泄露的狀況,因此繼續保持maxOpenConns=1bash

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的鏈接池是如何實現的

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()方法。

其主要作的工做是斷定鏈接是否過時,若是沒有過時則放回鏈接池。

鏈接池的完整實現邏輯以下圖所示

案例分析

有了前面的背景知識,咱們來分析下上面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()
複製代碼

心得體會

最近原本是在看gorm的源碼,也想過把gorm應用到咱們的項目組裏,可是由於一些二次開發以及性能問題,上馬gorm的計劃先擱置了。

而後在看到gorm代碼的時候發現不少地方仍是直接使用了database/sql,尤爲是鏈接池這塊的實現。

在看這塊代碼的時候,還發現了咱們項目的部分代碼中使用了rows遍歷,可是忘記添加defer rows.Close()的狀況。這種狀況通常不會有什麼問題,可是若是由於一些意外狀況致使提早退出遍歷,則可能會出現鏈接泄露的問題。

我的公衆號JackieZheng,歡迎關注~~~

相關文章
相關標籤/搜索