go中数据库连接池的学习

使用了gorm,gorm的连接池是直接使用的database/sql,所以需要学习一下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)
go中数据库连接池的学习_第1张图片

你可能感兴趣的:(数据库)