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,欢迎关注~~~

相关文章
相关标签/搜索