当我们拿到一个DB
实例之后就可以操作数据库了。本篇我们将更加深入database/sql
包,共同探讨连接池的维护
和请求的处理
。
上一篇我们一起学习了what on earth the DB object is
。同时我画了一张图进行说明:
上图中很多部分在上一篇中都还没有涉及到,因为
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
。
当获取到数据库连接之后,就可以基于这个连接进行真实的数据库操作了。
下一章我们将一起探讨真正的请求操作。