dive into golang database/sql(2)

当我们拿到一个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


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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,802评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,109评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,683评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,458评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,452评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,505评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,901评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,550评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,763评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,556评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,629评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,330评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,898评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,897评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,140评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,807评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,339评论 2 342

推荐阅读更多精彩内容

  • No.1 文档概要 在Golang中使用SQL或类似SQL的数据库的惯用方法是通过 database/sql 包操...
    尼古拉斯河马阅读 27,266评论 5 14
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,579评论 18 139
  • 上一章中我们一起探讨了golangdatabase/sql包中如何获取一个真实的数据库连接。当我们拿到一个数据库连...
    suoga阅读 534评论 0 1
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,717评论 6 342
  • 目标: 1、完成兴趣各营作业 a.画两幅画 b.每天练声训练 c.日更写三篇杂文 d.每天的感恩日记及感恩练习 2...
    皮儿米阅读 179评论 0 0