一、驱动重要接口
type Driver interface {
// 根据dsn得到一个连接
Open(name string) (Conn, error)
}
type DriverContext interface {
// 如果数据库驱动同时实现了这个接口,则会调用这个接口先获得一个连接器,
// 后续将通过这个连接器来获取连接,好处在于不用每次获取连接都解析dsn
OpenConnector(name string) (Connector, error)
}
type Connector interface {
// 获取一个连接的方法,需要数据库驱动实现
Connect(context.Context) (Conn, error)
// 返回数据库驱动
Driver() Driver
}
// 通常实现Conn的数据结构会实现Execer、ExecContext、Queryer、QueryerContext等这些接口
// 这样接口便具有了执行查询、操作类型sql的能力
type Conn interface {
// 数据库预编译模式,返回一个Stmt
Prepare(query string) (Stmt, error)
// 关闭一个连接的方法
Close() error
// 开始一个事务,返回Tx数据结构,该方法目前标为抑制,推荐使用ConnBeginTx接口开启事务
Begin() (Tx, error)
}
// 执行操作类SQL的功能
type ExecerContext interface {
ExecContext(ctx context.Context, query string, args []NamedValue) (Result, error)
}
type Result interface {
// 索引ID值
LastInsertId() (int64, error)
// 受影响行数
RowsAffected() (int64, error)
}
// 执行查询类SQL的功能
type QueryerContext interface {
QueryContext(ctx context.Context, query string, args []NamedValue) (Rows, error)
}
type Rows interface {
// 返回表中的列信息
Columns() []string
// 关闭方法,会清空缓存(即使未读取完),同时将连接放回池中
Close() error
// 实现dest的赋值
Next(dest []Value) error
}
// 事务功能接口
type Tx interface {
Commit() error
Rollback() error
}
二、SQL连接池实现
在go语言中,SDK除了为SQL定义了一套规范之外,还为数据库连接实现了一个数据库连接池。在go的sql实现中,如果要使用某种数据库,如MySQL,首先需要注册驱动,然后通过sql.Open方法并传入DSN字符串即可获得一个数据库DB实例,该实例实际提供了执行sql语句的能力,并实现了一个数据库连接池,当实际执行sql语句时,将决定根据策略选择从池中取出连接或者新建一个连接。如下,并将介绍其中一些重要的字段:
type DB struct {
waitDuration atomic.Int64
connector driver.Connector
numClosed atomic.Uint64
mu sync.Mutex
freeConn []*driverConn // 连接池实现,driverConn为driver.Conn多做一层包装得到
connRequests map[uint64]chan connRequest // 当numOpen大于maxOpen时,将阻塞请求并在此存储阻塞的请求
nextRequest uint64 // connRequests的key,全局单调递增
numOpen int // 已打开连接数,包括freeConn中的连接
openerCh chan struct{} // 后台有一协程通过此channel获取信号处理connRequests中的连接
closed bool
dep map[finalCloser]depSet
lastPut map[*driverConn]string
maxIdleCount int // 最大空闲连接数
maxOpen int // 最大打开连接数
maxLifetime time.Duration // 最大存活时间
maxIdleTime time.Duration // 最大空闲时间
cleanerCh chan struct{}
waitCount int64 // 等待连接数
maxIdleClosed int64 // 由于最大空闲而关闭的连接数
maxIdleTimeClosed int64 // 由于最大空闲时间而关闭的连接数
maxLifetimeClosed int64 // 由于最大存活时间而关闭的连接数
stop func()
}
下面主要来看看一条连接从创建到放回连接池或者关闭的整个流程。首先来看看一条数据库连接的创建,标准库将通过db.conn方法,根据策略选择新建连接还是优先从池中获取连接(当池中连接不够时仍然需要新建连接),方法源码如下:
Go
func (db *DB) conn(ctx context.Context, strategy connReuseStrategy) (*driverConn, error) {
......
// 如果策略为cachedOrNewConn并且池中有空闲连接,则优先从池中获取
last := len(db.freeConn) - 1
if strategy == cachedOrNewConn && last >= 0 {
// 从池中取出一个连接
conn := db.freeConn[last]
db.freeConn = db.freeConn[:last]
conn.inUse = true
if conn.expired(lifetime) {
db.maxLifetimeClosed++
db.mu.Unlock()
conn.Close()
return nil, driver.ErrBadConn
}
db.mu.Unlock()
......
return conn, nil
}
// 如果达到最大连接数,则不能获取连接,需要先阻塞等待
if db.maxOpen > 0 && db.numOpen >= db.maxOpen {
// 对于chan connRequest,后台会有一个新建连接的协程,
// 将在适当的时候将连接通过这个channel发送给当前请求,在此之前该请求将阻塞
req := make(chan connRequest, 1)
reqKey := db.nextRequestKeyLocked()
db.connRequests[reqKey] = req
db.waitCount++
db.mu.Unlock()
waitStart := nowFunc()
// 超时退出
select {
case <-ctx.Done():
......
return nil, ctx.Err()
case ret, ok := <-req:
db.waitDuration.Add(int64(time.Since(waitStart)))
if !ok {
return nil, errDBClosed
}
// 如果从池中取得了一个过期的连接,则关闭连接,返回driver.ErrBadConn错误
if strategy == cachedOrNewConn && ret.err == nil && ret.conn.expired(lifetime) {
db.mu.Lock()
db.maxLifetimeClosed++
db.mu.Unlock()
ret.conn.Close()
return nil, driver.ErrBadConn
}
......
// 获取到连接(可能是池中的连接也可能是新连接)
return ret.conn, ret.err
}
}
// 新建一个连接
db.numOpen++
db.mu.Unlock()
ci, err := db.connector.Connect(ctx)
if err != nil {
db.mu.Lock()
db.numOpen--
db.maybeOpenNewConnections()
db.mu.Unlock()
return nil, err
}
db.mu.Lock()
dc := &driverConn{
db: db,
createdAt: nowFunc(),
returnedAt: nowFunc(),
ci: ci,
inUse: true,
}
db.addDepLocked(dc, dc)
db.mu.Unlock()
return dc, nil
}
连接使用完毕后自然需要回收,如何回收连接?我们可以看到driverConn(实际为真实的数据库连接加了一层包装,用来维护一些状态信息)的releaseConn方法,这个方法会在一个查询或者一次事务等操作结束后被调用,源码如下:
func (dc *driverConn) releaseConn(err error) {
dc.db.putConn(dc, err, true)
}
可以看到,实际上调用了db的putConn方法,将连接放回连接池,只是在放回之前需要先进行一些前置校验等操作,如果该连接发生错误,则直接关闭连接而不放回连接池。
func (db *DB) putConn(dc *driverConn, err error, resetSession bool) {
...... // 前置校验等等操作
// 如果发生driver.ErrBadConn,直接关闭连接
if errors.Is(err, driver.ErrBadConn) {
db.maybeOpenNewConnections()
db.mu.Unlock()
dc.Close()
return
}
// 放回连接池的方法
added := db.putConnDBLocked(dc, nil)
db.mu.Unlock()
}
我们继续跟进db.putConnDBLocked方法。多提一句,可以看到putConnDBLocked后面带有Locked,说明这个方法是在锁中被调用的,所以它是并发安全的。进入putConnDBLocked方法,也并不复杂,源码如下所示:
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 {
var req chan connRequest
var reqKey uint64
for reqKey, req = range db.connRequests {
break
}
delete(db.connRequests, reqKey)
if err == nil {
dc.inUse = true
}
req <- connRequest{
conn: dc,
err: err,
}
return true
} else if err == nil && !db.closed {
// 这里就放回连接池了,如果没有发生错误的话
if db.maxIdleConnsLocked() > len(db.freeConn) {
db.freeConn = append(db.freeConn, dc)
db.startCleanerLocked()
return true
}
db.maxIdleClosed++
}
return false
}
一条连接的获取到释放的大致过程就如上面所示了,其中省略了一些代码防止过多的代码妨碍理解和把握整体过程。实际上还有新建连接和释放过期连接的后台协程参与到整个过程中,上述并未给出。
三、总结
在go中,标准库已经实现了数据库连接池的部分,这就意味着,诸如MySQL等驱动则只需要实现driver.Connector、driver.Conn等等接口,同时完成自身数据库通信协议的解析就好,而不再需要自己实现数据库连接池。当然,连接池已经被标准库实现也意味着我们不便自己去管理这些数据库连接,毕竟不能去修改标准库的源码。不过仍然也留了一些口子可以去操作,比如自己实现driver.Connector并在其中包含第三方驱动(如MySQL),这样也可以一定程度上自己去管理数据库的连接,从而实现一些诸如连接负载均衡的功能。