Go组件学习——database/sql数据库连接池你用对了吗

1、案例

case1: maxOpenConns > 1

func fewConns() {
	db, _ := db.Open("mysql", "root:[email protected]/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:[email protected]/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:[email protected]/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:[email protected]/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:[email protected]/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的微信公众号,我会将我的文章推送给您,并和您一起分享我日常阅读过的优质文章。

原文地址:https://www.cnblogs.com/bigdataZJ/p/database-sql-connection-pool.html

时间: 2024-10-09 19:49:31

Go组件学习——database/sql数据库连接池你用对了吗的相关文章

【转】JDBC学习笔记(8)——数据库连接池(dbcp&amp;C3P0)

转自:http://www.cnblogs.com/ysw-go/ JDBC数据库连接池的必要性 一.在使用开发基于数据库的web程序时,传统的模式基本是按一下步骤: 1)在主程序(如servlet/beans)中建立数据库连接 2)进行sql操作 3)断开数据库连接 二.这种模式开发,存在的问题: 1)普通的JDBC数据库连接使用DriverManager来获取,每次向数据库建立连接的时候都要将Connection加载进内存中,再验证用户名和密码(得花费0.05s~1s的时间).需要数据库连接

java学习笔记—第三方数据库连接池包1(29)

第一步:导入dbcp包 第二步:通过核心类连接数据 BasicDataSource它是javax.sql.DataSrouce的子类. 一个工具类:BasicDataSourceFactory. 手工通过代码连接数据库: BasicDataSource ds = new BasicDataSource(); //设置driver ds.setDriverClassName("com.mysql.jdbc.Driver"); //设置url ds.setUrl("jdbc:my

Flask(4):wtforms组件 &amp; 数据库连接池 DBUtils

wtforms 组件的作用: --- 生成 HTML 标签 --- form 表单验证 示例代码: app.py from flask import Flask, render_template, request from wtforms import Form from wtforms.fields import simple from wtforms.fields import core from wtforms.fields import html5 from wtforms import

[原创]java WEB学习笔记80:Hibernate学习之路--- hibernate配置文件:JDBC 连接属性,C3P0 数据库连接池属性等

本博客的目的:①总结自己的学习过程,相当于学习笔记 ②将自己的经验分享给大家,相互学习,互相交流,不可商用 内容难免出现问题,欢迎指正,交流,探讨,可以留言,也可以通过以下方式联系. 本人互联网技术爱好者,互联网技术发烧友 微博:伊直都在0221 QQ:951226918 -----------------------------------------------------------------------------------------------------------------

【Java EE 学习第16天】【dbcp数据库连接池】【c3p0数据库连接池】

零.回顾之前使用的动态代理的方式实现的数据库连接池: 代码: 1 package day16.utils; 2 3 import java.io.IOException; 4 import java.lang.reflect.InvocationHandler; 5 import java.lang.reflect.Method; 6 import java.lang.reflect.Proxy; 7 import java.sql.Connection; 8 import java.sql.D

SQL数据库优化:切割、数据库连接池--【考试系统】

上篇讲到了考试过程中,开发人员需要关注cpu和内存.sql日志也不容忽视,sql日志中显示了数据库操作系统的报错日志,给排错提供了很大的便利. 考试的数据库中写入了一些监听死锁和当前最耗资源语句的SQL语句.可以及时的监控死锁和了解当前考试进行到哪一步,是抽题,还是答题,还是交卷. 其中,听到了两个词:切割.数据库连接池. 后来查了一下,发现这两种方法都可以从不同的程度上对数据库的性能进行优化. 一.切割 横向切割: 就是把行分类,常用的两种是按照时间.索引划分.时间划分:比如5年的历史数据,根

SQL面试整理(1)——数据库连接池

在web开发中,如果JSP.Servlet或EJB使用JDBC直接访问数据库,每一次数据访问请求都必须经历建立数据库连接,打开数据库,存取数据库和关闭数据库连接等操作步骤,如果频繁发生这种数据库操作,系统的性能必然会急剧下降,甚至会导致系统崩溃.数据库连接池技术是解决这个问题最常用的方法,在许多应用程序服务器(例如:Weblogic,WebSphere,JBoss)中,基本都提供了这项技术. 数据库连接池负责分配.管理和释放数据库的连接. 1. 数据库连接复用.重复使用现有的数据库连接,可以避免

day13_Mysql事物与数据库连接池学习笔记

一.Mysql事务 事务: 事务指逻辑上的一组操作,组成这组操作的各个单元,要么全部成功,要么全部不成功(数据回滚). 例如:A给B转帐,对应于如下两条sql语句 : update account set money=money-100 where name='a'; update account set money=money+100 where name='b'; 1.Mysql中的事务 a.mysql引擎是支持事务的. b.mysql语句默认是自动提交事务.每条语句都处在单独的事务中. c

javaweb学习总结(三十九)——数据库连接池

一.应用程序直接获取数据库连接的缺点 用户每次请求都需要向数据库获得链接,而数据库创建连接通常需要消耗相对较大的资源,创建时间也较长.假设网站一天10万访问量,数据库服务器就需要创建10万次连接,极大的浪费数据库的资源,并且极易造成数据库服务器内存溢出.拓机.如下图所示: 二.使用数据库连接池优化程序性能 2.1.数据库连接池的基本概念 数据库连接是一种关键的有限的昂贵的资源,这一点在多用户的网页应用程序中体现的尤为突出.对数据库连接的管理能显著影响到整个应用程序的伸缩性和健壮性,影响到程序的性