本文记录了我在实际工作中关于数据库操作上一些小经验,也是新手入门golang时我认为一定会碰到问题,没有什么高大上的东西,所以希望能抛砖引玉,也算是对这个问题的一次总结。

其实我也是一个新手,机缘巧合几个月前开始做golang开发,以前一直是以.NET技术栈为主,文章如有错误不吝指正。

访问数据库

相信大家第一次碰到这个问题的时候应该和我一样,去网上找个例子参考一下。没错,这样的例子一搜一大把,于是我们很容易(抄)写了如下一段代码:

import (
"fmt"
"database/sql"
_ "github.com/go-sql-driver/mysql"
) func main() {
db, err := sql.Open("mysql","root:111111@tcp(127.0.0.1:3306)/testdb")
if err != nil {
panic(err)
}
err = db.Ping()
if err != nil {
panic(err)
}
fmt.Println("Successfully connected!")
}

把程序运行起来一看,成功地输出了想看到的东西,内心一阵暗喜“so easy"。于是把这段代码封装成一个公共方法供其他地方调用:

func GetDbContext() *sql.DB {
db, err := sql.Open("mysql","root:111111@tcp(127.0.0.1:3306)/testdb")
if err != nil {
panic(err)
}
err = db.Ping()
if err != nil {
panic(err)
}
return db
} func DoSomething(){
db := GetDbContext()
rows,_ := db.Query("select * from table1")
}

没错我最早就是这么干的,然后开始愉快地转头写CRUD了,不过事情可没这么简单。

很快, 编码五分钟捉虫两小时开场了。

慢慢的我就发现,在连续多次操作数据库后就偶尔发生程序卡死的情况,请求一直是pending状态,只能杀死进程重启才可以。刚开始没在意,也没有怀疑是数据库操作有问题,但后来越来越频繁严重影响到程序开发,没办法就记log加断点调试看是哪里出了问题。经过反复验证后确定问题就出在执行SQL语句这里,这下懵了,我看网上大家都是这么写的怎么会有问题??

连接池问题

根据多年开发经验,大胆猜测SQL执行失败最大的可能性就是数据库连接不上,在确认数据库没有崩掉的情况下开始研究代码哪里写的不对,但是前后也就那么几行代码实在看不出什么毛病,只能开始深入了研究database/sql包的知识点。

通过查资料发现open完数据库后的返回对象sql.DB实际上是一个连接池对象,并不是单纯的某一个连接。它是一个抽象的数据访问接口,和数据库类型无关,当然也就和具体的数据库Schema无关。我们要实现某一个数据库的访问单纯用这个包是不够的,还要引入具体的数据库驱动包,这个驱动才是真正实现数据库访问的东西。

现在再回过头来看代码,既然open创建了连接池,那用完把它销毁不就好了,于是参考官网文档稍加改进:

func GetDbContext() *sql.DB {
db, err := sql.Open("mysql","root:111111@tcp(127.0.0.1:3306)/testdb")
if err != nil {
panic(err)
}
err = db.Ping()
if err != nil {
panic(err)
}
return db
} func DoSomething(){
db := GetDbContext()
defer db.Close()
rows,_ := db.Query("select * from table1")
}

看似行得通,但是估计没人愿意这样做。原因很明显,别的先不谈,创建和销毁连接池开销太大了,你这样对它于心何忍,拿着屠龙刀去砍柴。

使用连接池的好处就是不需要开发者频繁地创建和销毁连接,这两项工作都交给了连接池去做,我们只需要在使用前找它要一个可用的连接,用完还回去就可以了。

这里引用一段官方文档中的原话:

Although it’s idiomatic to Close() the database when you’re finished with it, the sql.DB object is designed to be long-lived. Don’t Open() and Close() databases frequently. Instead, create one sql.DB object for each distinct datastore you need to access, and keep it until the program is done accessing that datastore. Pass it around as needed, or make it available somehow globally, but keep it open. And don’t Open() and Close() from a short-lived function. Instead, pass the sql.DB into that short-lived function as an argument.

核心意思就是sql.DB是一个长生命周期对象,你不要随便打开和关闭,并且建议你在程序中为每一个数据库创建唯一的sql.DB

那么现在的问题就是如何保证程序中只有一个连接池呢?

很简单,使用一个全局变量即可,有点类似C#和java中static的味道,在Golang中可以使用如下方法声明一个全局对象:

package demo

import (
"database/sql"
) var mydb,_ = sql.Open("mysql","connection_string")

不过我们的业务场景比较特殊,系统中有很多个数据库,要根据不同参数去连不同数据库,那么上面这种声明赋值方式就不行了,我稍加改进,结合map实现了连接池动态管理:

var envdbMap map[string]*sql.DB

func GetEnvDbContext(connector config.DbConnector) *sql.DB {
if envdbMap == nil {
envdbMap = make(map[string]*sql.DB)
} db, ok := envdbMap[connector.ID]
if ok {
return db
} else {
connStr := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", connector.Host, connector.Port, connector.UserName, connector.Password, connector.DatabaseName)
db, err := sql.Open("postgres", connStr) envdbMap[connector.ID] = db
return db
}
}

原理很简单,就是用map装池子,池子装连接。

有借有还

到这里连接池已经准备好了,那么如何从池子中取一个可用的连接呢?这点池子已经帮大家考虑的很周到了,大家不需要写额外代码去获取连接,直接拿起池子用就可以了,内部会有一系列机制帮你弄到一个连接去执行SQL,以后有机会对池子的原理来做个解析。

但是用完要记得还回去,这个必须你手动去做,例如:

rows,_ := db.Query("select * from table1")
defer rows.Close()
// do sth...

最好不要在do sth之后做Close,因为一旦你这个过程中发生异常,导致后面的Close无法执行,那么这个连接就一直被占用,日积月累TCP连接就被你耗光了

官方文档说了,rows.Close()是一种无害(harmless)操作,你可以做多次,但是不能忘了做。

这里有个特殊情况要注意,对于那种没有返回结果的SQL语句,千万不要使用Query方法去执行,这会导致无法回收连接,这时候推荐使用Exec方法去执行。

配置连接池

默认情况下连接池没有数量限制,但是我们的机器有TCP的数量限制,不要因为一个程序拖死一台机器,所以不推荐无限量的去使用。database/sql包提供了几个连接池配置参数,主要包含:

  • db.SetMaxIdleConns(N) 设置空闲连接的数量
  • db.SetMaxOpenConns(N) 设置打开的连接数量
  • db.SetConnMaxLifetime(duration) 设置连接的生存时间

    详细的介绍大家可以参考官方文档。

总结

经过以上分析,可以清晰的知道最开始的bug就是因为错误地使用了连接池导致数据库连接被耗光从而无法执行SQL语句,其实说简单也很简单。

以上就是工作中使用golang访问数据库的踩坑历程,希望能帮到新接触golang的朋友,如有错误的地方欢迎指出,以免误导他人。

参考连接

http://go-database-sql.org/accessing.html

http://go-database-sql.org/retrieving.html

http://go-database-sql.org/connection-pool.html

在Golang中如何正确地使用database/sql包访问数据库的更多相关文章

  1. Golang中如何正确的使用sarama包操作Kafka?

    Golang中如何正确的使用sarama包操作Kafka? 一.背景 在一些业务系统中,模块之间通过引入Kafka解藕,拿IM举例(图来源): 用户A给B发送消息,msg_gateway收到消息后,投 ...

  2. golang——database/sql包学习

    1.database/sql包 sql包提供了保证SQL或类SQL数据库的泛用接口. 使用sql包时必须注入(至少)一个数据库驱动. (1)获取mysql driver:go get -v githu ...

  3. 使用Spring.net中对Ado.net的抽象封装来访问数据库

    使用Spring.net中对Ado.net的抽象封装来访问数据库     Spring.NET是一个应用程序框架,其目的是协助开发人员创建企业级的.NET应用程序.它提供了很多方面的功能,比如依赖注入 ...

  4. 关于Golang中database/sql包的学习

    go-sql-driver 请求一个连接的函数有好几种,执行完毕处理连接的方式稍有差别,大致如下: db.Ping() 调用完毕后会马上把连接返回给连接池. db.Exec() 调用完毕后会马上把连接 ...

  5. Golang中database/sql包

    驱动 github.com/go-sql-driver/mysql 请求一个连接的函数有好几种,执行完毕处理连接的方式稍有差别,大致如下: db.Ping() 调用完毕后会马上把连接返回给连接池. d ...

  6. Golang中Struct与DB中表字段通过反射自动映射 - sqlmapper

    Golang中操作数据库已经有现成的库"database/sql"可以用,但是"database/sql"只提供了最基础的操作接口: 对数据库中一张表的增删改查 ...

  7. go标准库的学习-database/sql

    参考:https://studygolang.com/pkgdoc 导入方式: import "database/sql" sql包提供了保证SQL或类SQL数据库的泛用接口. 使 ...

  8. go database/sql sql-driver/mysql 操作

    这里使用的是github.com/Go-SQL-Driver/MySQL, 所以需要下载一个github.com/Go-SQL-Driver/MySQL 引入 database/sql 和 githu ...

  9. golang中Context的使用场景

    golang中Context的使用场景 context在Go1.7之后就进入标准库中了.它主要的用处如果用一句话来说,是在于控制goroutine的生命周期.当一个计算任务被goroutine承接了之 ...

随机推荐

  1. mysql数据安全之利用二进制日志mysqlbinlog恢复数据

    mysql数据安全之利用二进制日志mysqlbinlog恢复数据 简介:如何利用二进制日志来恢复数据 查看二进制日志文件的内容报错: [root@xdclass-public log_bin]# my ...

  2. 拥抱 C/C++ : Android JNI 的使用

    编译工具 CMake 以及 Android 上 JNI 的使用介绍. 编译工具 CMake 在Android Studio 2.2 之后,工具中增加了 CMake 的支持,于是我们有两种选择来编译 c ...

  3. [leetcode]54. Spiral Matrix二维数组螺旋取数

    import java.util.ArrayList; import java.util.List; /** * Given a matrix of m x n elements (m rows, n ...

  4. msf ms17-010利用笔记

    nmap -sV -Pn xxxxx  扫描端口and系统信息 use auxiliary/scanner/smb/smb_ms17_010   扫描模块 set Rhosts 扫描目标 use ex ...

  5. [linux]makefile多目录

    在使用makefile多目录编写前需要掌握几个函数及符号 自定义变量 target=edit 引用的时候直接使用 $(target) 有点像C语言中的#define,这里的 $(target)会被替换 ...

  6. LInux学习笔记之常用命令

    以下命令主要是平时用到的命令,对于一些经常用到的,就收集资料,归纳一下. 指令目录: 1.yum命令: 2.wget命令: 3.tar命令: 4../configure,make,make insta ...

  7. 容器编排系统K8s之flannel网络模型

    前文我们聊到了k8s上webui的安装和相关用户授权,回顾请参考:https://www.cnblogs.com/qiuhom-1874/p/14222930.html:今天我们来聊一聊k8s上的网络 ...

  8. 原来大数据 Hadoop 是这样存储数据的

    HDFS概述 产生背景 随着数据量越来越大,在一个操作系统中存不下所有的数据.需要将这些数据分配到更多的操作系统中,带来的问题是多操作系统不方便管理和维护.需要一种系统来管理多台机器上的文件,这就是分 ...

  9. C#实现 Server-sent Events

    基于http协议交互的推送方法大概方法如下: 轮询(ajax),比较耗费服务器资源.COMET方式(COMET 技术并不是 HTML 5 ) websocket 双向数据推送,灵活,功能强大 Serv ...

  10. P4292 [WC2010]重建计划 点分治+单调队列

    题目描述 题目传送门 分析 看到比值的形式就想到 \(01分数规划\),二分答案 设当前的值为 \(mids\) 如果存在\(\frac{\sum _{e \in S} v(e)}{|S|} \geq ...