首页 > 数据库 >【转】dive into golang database/sql(2)

【转】dive into golang database/sql(2)

时间:2023-10-17 16:11:37浏览次数:33  
标签:connRequest err dive db golang freeConn sql connRequests 连接

转,原文: https://www.jianshu.com/p/807257fcb985?utm_campaign=studygolang.com&utm_medium=studygolang.com&utm_source=studygolang.com

 

----------

当我们拿到一个DB实例之后就可以操作数据库了。本篇我们将更加深入database/sql包,共同探讨连接池的维护请求的处理

上一篇我们一起学习了what on earth the DB object is。同时我画了一张图进行说明:

  DB
上图中很多部分在上一篇中都还没有涉及到,因为sql.Open方法仅仅就是返回这样一个DB对象并新开一个goroutine connectionOpener通过监听openerCh来新建连接。
本章我们将更加全面更加深入地介绍DB对象,学习它是如何创建连接并维护连接池的。

 

db.Query说起

继续那段最常见的代码:

db,_ := sql.Open("mysql", "xxx")
rows,_ := db.Query("SELECT age,name from student where score > ?", 85)
defer rows.Close()
for rows.Next() {
    var age int
    var name string
    _ = rows.Scan(&age, &name)
    fmt.Println(age, name)
}

上面的代码为了简便我忽略的所有的错误处理,但实际项目中你必须处理任何的错误!

当我们拿到db对象之后就可以进行Query了,那么Query背后到底发生了什么呢?源码非常简单,就只有几行:

// Query executes a query that returns rows, typically a SELECT.
// The args are for any placeholder parameters in the query.
func (db *DB) Query(query string, args ...interface{}) (*Rows, error) {
    var rows *Rows
    var err error
    for i := 0; i < maxBadConnRetries; i++ {
        rows, err = db.query(query, args, cachedOrNewConn)
        if err != driver.ErrBadConn {
            break
        }
    }
    if err == driver.ErrBadConn {
        return db.query(query, args, alwaysNewConn)
    }
    return rows, err
}

其实这个Query方法只是做了一层简单的包装,仅从这里我们依然看不出具体的行为,但是我们能够了解到的是,如果错误是driver.ErrBadConn的话,sql包默认帮我们做了maxBadConnRetries次重试。

// maxBadConnRetries is the number of maximum retries if the driver returns
// driver.ErrBadConn to signal a broken connection before forcing a new
// connection to be opened.
const maxBadConnRetries = 2

那我们再继续深入看看db.query方法究竟做了哪些工作:

func (db *DB) query(query string, args []interface{}, strategy connReuseStrategy) (*Rows, error) {
    ci, err := db.conn(strategy)
    if err != nil {
        return nil, err
    }

    return db.queryConn(ci, ci.releaseConn, query, args)
}

当然,细节也不明显。不过不用急,一步一步来。可以发现db.query做了两件事情:

  • 根据某种策略(strategy)获取一个数据库连接
  • 基于这个连接进行query操作

其实,所有的数据库操作都是这样:

  • 先获取数据库连接
  • 基于此连接执行目标指令

接下来,我们将重点看看获取数据库连接这部分的实现。

获取数据库连接

获取数据库连接的db.conn方法稍微有点长(60行左右),这里我给一个简略的伪代码版本:

func (db *DB) conn(strategy xxx) (*driverConn, error) {
    lock()
    defer unlock()
    if strategy==cachedOrNewConn && anyFreeConnCanReuse(db.freeConn) {
        conn := getOneConnFrom(db.freeConn)
        maintain(db.freeConn)
        return conn,nil
    }
    if db.maxOpen > 0 && db.numOpen >= db.maxOpen {
        db.connRequests = append(db.connRequests, dontReleaseConnToFreeConnGiveIt2MeInstead)
        ret := <- dontReleaseConnToFreeConnGiveIt2MeInstead
        return ret.conn,nil
    }
    conn := openANewConn(db.driver)
    maintainSomeInfo()
    return conn,nil
}

从伪代码里可以看出,获取一个数据库连接分三种情况:

  • 如果获取策略是cachedOrNewConn,就从现有的连接池里取一个空闲连接
  • 如果连接池里无可用连接,而连接数又已经到达配置的上限值,就发送一个坐等连接的通知,然后阻塞地在这里等等待(其它地方释放连接时会优先处理坐等连接的通知请求)
  • 如果连接池无可用连接,而现有连接数还没有达到配置的最大值,就通过driver再新建一个连接。

上面db.freeConn其实就是一个[]*driverConn,里面存放了空闲的数据库连接。

比较有意思的是第二点中的坐等连接,怎么个坐等法呢?看看实际代码就明白了:

if db.maxOpen > 0 && db.numOpen >= db.maxOpen {
        // Make the connRequest channel. It's buffered so that the
        // connectionOpener doesn't block while waiting for the req to be read.
        req := make(chan connRequest, 1)
        db.connRequests = append(db.connRequests, req)
        db.mu.Unlock()
        ret, ok := <-req
        if !ok {
            return nil, errDBClosed
        }
        if ret.err == nil && ret.conn.expired(lifetime) {
            ret.conn.Close()
            return nil, driver.ErrBadConn
        }
        return ret.conn, ret.err
    }

先看看connRequest的定义:

// connRequest represents one request for a new connection
// When there are no idle connections available, DB.conn will create
// a new connRequest and put it on the db.connRequests list.
type connRequest struct {
    conn *driverConn
    err  error
}

db.connRequests其实就是[]chan connRequest

所以坐等连接其实就是,把一个connRequest放入db.connRequests中,等待它被填充。当它被填充过了,于是我们就可以从它里面拿到数据库连接了。

“喂!db大哥!现在新建不了连接了,但是我急着要,你那儿有了空闲的就赶紧帮我放到connRequest里面,我在这儿等着呢”

那么到底是什么时候db会去填充这个connRequest?猜猜看?

很容易想到,是在释放连接的时候。每当一个连接使用完毕想要释放时,通常会想到将它放入freeConn队列中。这时,可以先检测connRequests中有没有坐等连接的请求,有的话就可以把连接分给那个请求,而不是放进freeConn。这也符合freeConn的定义,既然有任务等着用连接,显然freeConn里是不应该有连接的。但到底是不是这样的呢?一起看看代码:

// Satisfy a connRequest or put the driverConn in the idle pool and return true
// or return false.
// putConnDBLocked will satisfy a connRequest if there is one, or it will
// return the *driverConn to the freeConn list if err == nil and the idle
// connection limit will not be exceeded.
// If err != nil, the value of dc is ignored.
// If err == nil, then dc must not equal nil.
// If a connRequest was fulfilled or the *driverConn was placed in the
// freeConn list, then true is returned, otherwise false is returned.
func (db *DB) putConnDBLocked(dc *driverConn, err error) bool {
    if db.closed {
        return false
    }
    if db.maxOpen > 0 && db.numOpen > db.maxOpen {
        return false
    }
    if c := len(db.connRequests); c > 0 {
        req := db.connRequests[0]
        // This copy is O(n) but in practice faster than a linked list.
        // TODO: consider compacting it down less often and
        // moving the base instead?
        copy(db.connRequests, db.connRequests[1:])
        db.connRequests = db.connRequests[:c-1]
        if err == nil {
            dc.inUse = true
        }
        req <- connRequest{
            conn: dc,
            err:  err,
        }
        return true
    } else if err == nil && !db.closed && db.maxIdleConnsLocked() > len(db.freeConn) {
        db.freeConn = append(db.freeConn, dc)
        db.startCleanerLocked()
        return true
    }
    return false
}

首先解释一下方法名putConnDBLocked。在sql包中,如果某个方法当且仅当会在加锁的情况下被调用,那么就会给这个方法加上Locked的后缀,方便开发者理解。

putConnDBLocked方法中,首先会去检测db.connRequests里是否有坐等连接的请求,如果有的话就用当前要释放的连接去满足那个请求。只有当发现没有请求时,才会把连接放到freeConn中。

这有一个问题:

为什么不把所有的连接全部释放到一个channel里,任何需要连接的都通过 conn <- bufferedChan这样的方式统一来处理,而要选择用freeConn和connRequests两个slice来曲折地实现呢?

我觉得作者主要考虑的问题是公平性。如果多个goroutine同时在取某个channel,那么当channel中新加一条消息时,无法确定这条消息被谁取走了,大家的机会都是均等的。在极端情况下,这可能出现某个等着获取连接的请求永远取不到连接。

使用connRequest对请求进行排队,这样可以让先等待的一方在有连接可用时可以先用上。但是对于每次取队首元素的场景,代码实现为什么会选择用slice而不是链表?

req := db.connRequests[0]
// This copy is O(n) but in practice faster than a linked list.
copy(db.connRequests, db.connRequests[1:])
db.connRequests = db.connRequests[:c-1]

代码中有注释说:

虽然copy是O(n)的复杂度,但是实际情况是比链表更快。

copy具体的实现由于在汇编代码里所以暂时没有看,如果真的是不输于链表的话,我猜测copy(s1, s2)执行的其实类似于

s1.Head = s2.Head

如果是这样的话,那copy确实性能很好。

后续我会专门写一篇文章来分析builtin copy


当获取到数据库连接之后,就可以基于这个连接进行真实的数据库操作了。

下一章我们将一起探讨真正的请求操作。



作者:suoga
链接:https://www.jianshu.com/p/807257fcb985
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

标签:connRequest,err,dive,db,golang,freeConn,sql,connRequests,连接
From: https://www.cnblogs.com/oxspirt/p/17769957.html

相关文章

  • Ubuntu 中 Mysql的简单使用
    起因:建立web服务器时需要使用数据库去保存用户名和密码。//建立yourdb库createdatabaseyourdb;//创建user表USEyourdb;CREATETABLEuser(usernamechar(50)NULL,passwdchar(50)NULL)ENGINE=InnoDB;//添加数据INSERTINTOuser(username,passwd)......
  • 数据库SQL实战|牛客网(查找入职员工时间排名倒数第三的员工所有信息)
    描述有一个员工employees表简况如下: 请你查找employees里入职员工时间排名倒数第三的员工所有信息,以上例子输出如下:输出:10005|1955-01-21|Kyoichi|Maliniak|M|1989-09-12droptableifexists`employees`;CREATETABLE`employees`(`emp_no`int(11)NOTNULL,`bir......
  • 首页database SQL Server异常代码处理的深入讲解
    SQLServer异常代码处理的深入讲解在SQLServer中,异常代码处理(ExceptionHandling)是非常重要的技术之一。良好的异常处理可以帮助我们更好地处理运行中的错误,保障系统的稳定性和安全性。本文将深入讲解SQLServer异常代码处理,包括常见的异常类型、异常处理方式及异常处理的最......
  • Oracle中如何根据查询sql片段定位查询客户端主机地址
    --1.根据sql片段获取sql_id--select*fromv$sqlwherelast_active_time>sysdateandsql_textlike'%ZL0204_03r%';selectsql_id,modulefromv$sqlwherelast_active_time>sysdateandsql_textlike'%ZL0204_03%';--2.根据sql_id查询sid......
  • docker 安装 nginx tomcat mysql
    dockersearchnginxdockerpullnginx//在root目录下创建nginx目录用于储层nginx数据信息mkdir~/nginxcdnginxmkdirconfcdconf把nginx.conf文件拷贝到~/nginx/con目录下cd~/nginxdockerrum-id--namenginx01\-p80:80\-v$PWD/conf/nginx.conf:/etc/nginx/n......
  • MySQL八股基础
    1.执行一条select语句,期间会发生什么?连接器:建立连接,管理连接,校验用户身份。查询缓存,查询语句命中直接返回,否则继续往下执行。MySQL8.0删除。解析器:解析SQL语句,对select语句执行词法分析、语法分析,构建语法树,方便后续模块读取表名、字段、语句类型。执行SQL语句。分为3个阶......
  • MySQL基本语法和数字马力笔试
    1.DROPDATABASENAME;删除名字为NAME的数据库2.VARCHAR(10);可存储的最大字符长度为103.SELECTIDFROM表;从表中检索出ID的列4.DISTINCT搜索去重5.select两列时,用逗号分隔开6.排序orderby(默认升序,desc降序)7.按两种排序orderbya,b;8.selectquantity,item_pricefromOrderIt......
  • 禅道V16.5SQL注入漏洞(CNVD-2022-42853)
    禅道V16.5SQL注入漏洞(CNVD-2022-42853)0x01介绍禅道项目管理软件是一款国产的、基于LGPL协议、开源免费的项目管理软件,它集产品管理、项目管理、测试管理于一体,同时还包含了事务管理、组织管理等诸多功能,是中小型企业项目管理的首选,基于自主的PHP开发框架──ZenTaoPHP而成,第三......
  • linux安装mysql5.7
    wget-i-chttp://dev.mysql.com/get/mysql57-community-release-el7-10.noarch.rpmyum-yinstallmysql57-community-release-el7-10.noarch.rpmyum-yinstallmysql-community-server--nogpgchecksystemctlstartmysqld.servicesystemctlstatusmysqld.servi......
  • Linux MySQL8.1 二进制安装和配置
    下载bin文件https://dev.mysql.com/downloads/mysql/选择LinuxGeneric,下载对应的tar包解压tar包解压完成后,目录结构如下:lsbindocsincludelibLICENSEmanREADMEsharesupport-files每个目录的内容,官方文档有说明DirectoryContentsofDirectorybi......