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的微信公众号,我会将我的文章推送给您,并和您一起分享我日常阅读过的优质文章。
Go组件学习——database/sql数据库连接池你用对了吗的更多相关文章
- go标准库的学习-database/sql
参考:https://studygolang.com/pkgdoc 导入方式: import "database/sql" sql包提供了保证SQL或类SQL数据库的泛用接口. 使 ...
- SQL数据库连接池与C#关键字return
SQL数据库连接池: 先前做的一个Sharepoint项目,在上线后的不久,最近一直出现间歇性访问缓慢问题Sharepoint特性问题,并分析了其数据库服务器,发现所耗内存已经达到了97%. 所以断定 ...
- JDBC 学习复习6 学习与编写数据库连接池
之前的工具类DBUtil暴露的问题 用户每次请求都需要向数据库获得链接,而数据库创建连接通常需要消耗相对较大的资源,创建时间也较长.假设网站一天10万访问量,数据库服务器就需要创建10万次连接,极大的 ...
- JDBC学习笔记(8)——数据库连接池(dbcp&C3P0)
JDBC数据库连接池的必要性 一.在使用开发基于数据库的web程序时,传统的模式基本是按一下步骤: 1)在主程序(如servlet/beans)中建立数据库连接 2)进行sql操作 3)断开数据库连接 ...
- JDBC编程学习笔记之数据库连接池的实现
在JDBC编程的时候,获取到一个数据库连接资源是很宝贵的,倘若数据库访问量超大,而数据库连接资源又没能得到及时的释放,就会导致系统的崩溃甚至宕机.造成的损失将会是巨大的.再看有了数据库连接池的JDBC ...
- Go组件学习——手写连接池并没有那么简单
1.背景 前段时间在看gorm,发现gorm是复用database/sql的连接池. 于是翻了下database/sql的数据库连接池的代码实现,看完代码,好像也不是很复杂,但是总觉得理解不够深刻,于 ...
- 【转】JDBC学习笔记(8)——数据库连接池(dbcp&C3P0)
转自:http://www.cnblogs.com/ysw-go/ JDBC数据库连接池的必要性 一.在使用开发基于数据库的web程序时,传统的模式基本是按一下步骤: 1)在主程序(如servlet/ ...
- go标准库的学习-database/sql/driver
参考:https://studygolang.com/pkgdoc 1>导入方式: import "database/sql/driver" driver包定义了应被数据库驱 ...
- java学习笔记—第三方数据库连接池包1(29)
第一步:导入dbcp包 第二步:通过核心类连接数据 BasicDataSource它是javax.sql.DataSrouce的子类. 一个工具类:BasicDataSourceFactory. 手工 ...
随机推荐
- [Spring-Cloud-Alibaba] Sentinel 规则持久化
在之前的练习中,只要应用重启,就需要重新配置,这样在我们实际的项目是非常不实用的,那么有没有办法把我们配置的规则保存下来呢?答案是YES,那么接下来,给大家来介绍如何将Sentinel规则持久化. D ...
- sql LocalDB 的安装环境和使用方法
LocalDB LocalDB专门为开发商.它是非常容易安装,无需管理,但它提供了相同的T-SQL语言,编程表面和客户端供应商定期的SQL Server Express.实际上,目标SQL Serve ...
- 《VR入门系列教程》之12---转换矩阵
转换矩阵 模型网格的三维空间位置都是由它们的顶点坐标决定的,如果每次想要移动一下模型位置都要依次改变每个网格的顶点坐标,这将一件非常头疼的事,要是遇上需要显示动画效果那就更糟了.为了解决这个问 ...
- Java NIO DirectByteBuffer 的使用与研究
一.结论 DirectByteBuffer 与 ByteBuffer 最大区别就在于缓冲区内存管理的方式.ByteBuffer使用的是堆内存,DirectByteBuffer 使用的是堆外内存,堆外内 ...
- golang "[]uint8" to string
关于Uinit8和Byte: The Go Programming Language Specification Numeric types uint8 the set of all unsigned ...
- Spring源码分析之环境搭建
写在最前面 最近突然心血来潮,想看看源码,看看大牛都怎么码代码,膜拜下.首选肯定是spring大法,于是说干就干,从GitHub上下载spring-framework源码编译拜读. 环境搭建 安装JD ...
- Javaweb入门 数据库第一天
数据库概述 本菜鸟使用的数据库软件为Mariadb,以下内容都是以Mariadb数据库软件来写的学习总结. 数据库 所谓的数据库就是用于存储.管理数据的仓库,数据库根据底层存储数据结构的不同可以分为很 ...
- java.sql.SQLException: Parameter index out of range (0 < 1 ).
向SQL中传入数据是从1开始的!!! 从ResultSet中取数据也是从1开始的!
- Git/Github使用方法小记
今天把人间网的桌面客户端renjian-deck正式开源了,之前对javascript的了解其实非常的不够的,所以这一次的代码写的也是乱七八糟重用性及其低下,虽然我无数次的想把代码重新整理一下,不过还 ...
- coffeescript 函数 箭头表达式
函数 do可以形成闭包,使方法作用域不受外部变化的影响. 隐式返回最后一个表达式的值 函数调用省略括号 用arguments数组访问传递给函数的所有对象(低可读性) @name为this.name的简 ...
