dive into golang database/sql(2)

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

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

dive into golang database/sql(2)_第1张图片
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


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

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

你可能感兴趣的:(dive into golang database/sql(2))