使用了gorm,gorm的连接池是直接使用的database/sql,所以需要学习一下database/sql
1、驱动注册
import (
"database/sql"
_ "github.com/go-sql-driver/mysql"//自动执行init()函数
)
init的时候会自动注册
2、打开DB句柄
db, err := gorm.Open("mysql", "root:123456@(127.0.0.1:3306)/vf")
if err != nil {
fmt.Println("Open mySQL err:", err)
return nil
}
db.DB().SetMaxIdleConns(conf.MaxIdleConns) //设置最大空闲连接
db.DB().SetMaxOpenConns(conf.MaxOpenConns) //设置最大连接数
gorm的句柄结构:
type DB struct {
sync.RWMutex
Value interface{}
Error error
RowsAffected int64
// single db
db SQLCommon
blockGlobalUpdate bool
logMode logModeValue
logger logger
search *search
values sync.Map
// global db
parent *DB
callbacks *Callback
dialect Dialect
singularTable bool
// function to be used to override the creating of a new timestamp
nowFuncOverride func() time.Time
}
database/sql的句柄结构:
type DB struct {
// Atomic access only. At top of struct to prevent mis-alignment
// on 32-bit platforms. Of type time.Duration.
waitDuration int64 // Total time waited for new connections.
connector driver.Connector
// numClosed is an atomic counter which represents a total number of
// closed connections. Stmt.openStmt checks it before cleaning closed
// connections in Stmt.css.
numClosed uint64
mu sync.Mutex // protects following fields
freeConn []*driverConn
connRequests map[uint64]chan connRequest
nextRequest uint64 // Next key to use in connRequests.
numOpen int // number of opened and pending open connections
// Used to signal the need for new connections
// a goroutine running connectionOpener() reads on this chan and
// maybeOpenNewConnections sends on the chan (one send per needed connection)
// It is closed during db.Close(). The close tells the connectionOpener
// goroutine to exit.
openerCh chan struct{}
resetterCh chan *driverConn
closed bool
dep map[finalCloser]depSet
lastPut map[*driverConn]string // stacktrace of last conn's put; debug only
maxIdle int // zero means defaultMaxIdleConns; negative means 0
maxOpen int // <= 0 means unlimited
maxLifetime time.Duration // maximum amount of time a connection may be reused
cleanerCh chan struct{}
waitCount int64 // Total number of connections waited for.
maxIdleClosed int64 // Total number of connections closed due to idle.
maxLifetimeClosed int64 // Total number of connections closed due to max free limit.
stop func() // stop cancels the connection opener and the session resetter.
}
gorm的Open其实还是调用了database/sql 的 Open,只是对句柄进行了封装。
func Open(dialect string, args ...interface{}) (db *DB, err error) {
if len(args) == 0 {
err = errors.New("invalid database source")
return nil, err
}
var source string
var dbSQL SQLCommon
var ownDbSQL bool
switch value := args[0].(type) {
case string:
var driver = dialect
if len(args) == 1 {
source = value
} else if len(args) >= 2 {
driver = value
source = args[1].(string)
}
dbSQL, err = sql.Open(driver, source)
ownDbSQL = true
case SQLCommon:
dbSQL = value
ownDbSQL = false
default:
return nil, fmt.Errorf("invalid database source: %v is not a valid type", value)
}
db = &DB{
db: dbSQL,
logger: defaultLogger,
callbacks: DefaultCallback,
dialect: newDialect(dialect, dbSQL),
}
db.parent = db
if err != nil {
return
}
// Send a ping to make sure the database connection is alive.
if d, ok := dbSQL.(*sql.DB); ok {
if err = d.Ping(); err != nil && ownDbSQL {
d.Close()
}
}
return
}
到此为止,都还是没有真正的打开连接。真正打开连接是在执行到query、exec等方法的时候。
3、获取连接
func (db *DB) conn(ctx context.Context, strategy connReuseStrategy) (*driverConn, error) {
......
// 如果从连接池取连接
numFree := len(db.freeConn)
if strategy == cachedOrNewConn && numFree > 0 {
conn := db.freeConn[0]
copy(db.freeConn, db.freeConn[1:])
db.freeConn = db.freeConn[:numFree-1]
conn.inUse = true
db.mu.Unlock()
.......
// 这里要注意,从连接池里拿到的有可能是还没重置会话的连接,所以要多一层lastErr的判断
conn.Lock()
err := conn.lastErr
conn.Unlock()
if err == driver.ErrBadConn {
conn.Close()
return nil, driver.ErrBadConn
}
return conn, nil
}
// 如果连接池没有空闲的连接,则生成一个连接请求到db.connRequestszh中,并且阻塞等待连接的到来
if db.maxOpen > 0 && db.numOpen >= db.maxOpen {
req := make(chan connRequest, 1)
reqKey := db.nextRequestKeyLocked()
db.connRequests[reqKey] = req
db.waitCount++
db.mu.Unlock()
waitStart := time.Now()
// Timeout the connection request with the context.
select {
case <-ctx.Done():
........
return nil, ctx.Err()
case ret, ok := <-req:
atomic.AddInt64(&db.waitDuration, int64(time.Since(waitStart)))
........
ret.conn.Lock()
// 这里要注意,从连接池里拿到的有可能是还没重置会话的连接,所以要多一层lastErr的判断
err := ret.conn.lastErr
ret.conn.Unlock()
if err == driver.ErrBadConn {
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(),
ci: ci,
inUse: true,
}
db.addDepLocked(dc, dc)
db.mu.Unlock()
return dc, nil
}
有两次获取conn.lastErr的错误,是因为连接放回连接池过程中,会发送db.resetterCh,而这个重置会话的chan只有50个buffer(见OpenDB时make(chan *driverConn, 50)),所以有可能会重现阻塞的情况,当出现阻塞时,会标志这个连接为driver.ErrBadConn。
当重新建立连接(没有空闲的连接或没有连接池数量配置时),如果出现err,则会再调用db.maybeOpenNewConnections()异步创建一个连接。
4、连接回收到连接池
1)首先遍历dc.onPut,执行fn()
2)如果发现该连接不可用,则调用maybeOpenNewConnections() 异步创建一个连接,并且关闭不可用的连接。
3)如果连接成功被连接池回收,但db.resetterCh 阻塞了,则先标记连接为ErrBadConn,所以前面从连接池获取连接时每一次都会判断连接是否可用。
4)如果连接池满了,没回收成功,则会关闭该连接。
5、处理过期连接
1)开定时器,每隔一段时间检测空闲连接池中的连接是否过期
2)如果接收到db.cleanerCh的信号,也会遍历处理超时,db.cleanerCh的buffer只有1,一般在SetConnMaxLifetime检测生命周期配置变短时发送。
3)为了遍历空闲队列里面连接的公平性,做了一个巧妙的处理,一旦发现队列前面的连接过期,则会把最后一个连接放到最前面,然后从当前开始遍历。
4)遍历空闲队列发现超时的连接,把超时连接一个一个追加到关闭队列中append(closing, c),然后遍历关闭的队列,一个一个关闭。
最后附上一张database/sql连接池实现拓扑图:
(图片出处:https://blog.csdn.net/gaifyyang/article/details/103288928)