Golang數據庫連接池運行原理(源碼解析)

1.MySQL驅動註冊即連接池啓動

github.com/go-sql-driver/mysql/driver.go中的init方法實現mysql驅動註冊

func init() {
	sql.Register("mysql", &MySQLDriver{})
}

上面的init方法實際是調用"database/sql/driver"基礎包中的Register()方法,如下:

func Register(name string, driver driver.Driver) {
	driversMu.Lock()
	defer driversMu.Unlock()
	if driver == nil {
		panic("sql: Register driver is nil")
	}
	if _, dup := drivers[name]; dup {
		panic("sql: Register called twice for driver " + name)
	}
    // drivers = make(map[string]driver.Driver)
    // drivers是個map,註冊只是將“mysql”和一個空結構體MySQLDriver的指針存入map
	drivers[name] = driver
}

在註冊驅動時不會真正連接數據庫,只有在調用Open()方法的時候纔會連接,如下:

func Open(driverName, dataSourceName string) (*DB, error) {
	driversMu.RLock()
    // 獲取驅動結構體指針
	driveri, ok := drivers[driverName]
	driversMu.RUnlock()
	if !ok {
		return nil, fmt.Errorf("sql: unknown driver %q (forgotten import?)", driverName)
	}

	if driverCtx, ok := driveri.(driver.DriverContext); ok {
        // 調用驅動中的OpenConnector
        // func (d MySQLDriver) OpenConnector(dsn string) (driver.Connector, error) {
	    //    cfg, err := ParseDSN(dsn)
	    //    if err != nil {
		//        return nil, err
        //    }
        //    return &connector{
        //        cfg: cfg,
        //    }, nil
        // }
        // 實際是返回了dsn對應Config結構體指針
		connector, err := driverCtx.OpenConnector(dataSourceName)
		if err != nil {
			return nil, err
		}
        //func OpenDB(c driver.Connector) *DB {
        //	ctx, cancel := context.WithCancel(context.Background())
        //	db := &DB{
        //		connector:    c,
        //		openerCh:     make(chan struct{}, connectionRequestQueueSize),
        //      這裏默認chan的容量是50,高併發會不會產生大量的阻塞???
        //		resetterCh:   make(chan *driverConn, 50),
        //		lastPut:      make(map[*driverConn]string),
        //		connRequests: make(map[uint64]chan connRequest),
        //		stop:         cancel,
        //	}
        //
        //  啓動創建連接gotoutine
        //	go db.connectionOpener(ctx)
        //  啓動清理session的goroutine
        //	go db.connectionResetter(ctx)
        //
        //	return db
        //}
        // 這裏的OpenDB方法是初始化DB結構體
		return OpenDB(connector), nil
	}
    // 這一句是在轉換driver.DriverContext接口失敗時執行
    // 因爲go1.10之前默認是一下方法,1.10後使用driver.DriverContext接口
    // 下面是爲了做兼容
	return OpenDB(dsnConnector{dsn: dataSourceName, driver: driveri}), nil
}

2.sql.DB結構體

爲什麼要介紹這個結構體,因爲連接池的主要實現就是基於sql.DB和sql.driverConn來實現的。

type DB struct {
    // 是一個包含Connect(context.Context) (Conn, error)和Driver() Driver方法的接口。
    // 這兩個接口是需要實際的數據庫驅動來實現,再使用;
	connector driver.Connector
    // 是自sql.DB創建後關閉過的連接數;
	numClosed uint64
	mu           sync.Mutex
    // 實際空閒連接數;
	freeConn     []*driverConn
    // 存儲pending連接的map,當numOpen大於maxOpen時,連接會被暫存到該map中;
	connRequests map[uint64]chan connRequest
    // 即connRequests的key,記錄當前可用的最新的connRequest的key;
	nextRequest  uint64 
    // 當前活躍連接數和當前pending的連接數的總和
	numOpen      int    
    // 標記創建連接的的通知channel,獨立的goroutine異步消費該channel去執行創建;
	openerCh    chan struct{}
    // 負責重置session的channel,獨立的goroutine異步消費該channel去執行重置;
	resetterCh  chan *driverConn
    // 標記DB是否關閉;
	closed      bool
    // 記錄db與conn之間的依賴關係,維持連接池以及關閉時使用;
	dep         map[finalCloser]depSet
    // 最新入棧的連接,debug時使用,string中存的是棧buf相關信息;
	lastPut     map[*driverConn]string
    // 最大空閒連接數;
	maxIdle     int  
    // 最大連接數;
	maxOpen     int         
    // 控線連接的最大存活時間;
	maxLifetime time.Duration    
    // 標記超時連接的通知channel;
	cleanerCh   chan struct{}
    // 通過context通知關閉connection opener和session resetter;
	stop func() 
}

3.申請連接過程

go從連接池中獲取連接時有兩個策略:

// 請求新的連接
alwaysNewConn connReuseStrategy = iota
// 從連接池中獲取連接
cachedOrNewConn

這裏以一個普通查詢過程爲例,看下連接池如何工作:

// 一般查詢代碼
rows, err := dbt.db.Query("SELECT * FROM test")
// Query如下,包了QueryContext方法
func (db *DB) Query(query string, args ...interface{}) (*Rows, error) {
    // context.Background()是爲了創建一個根上下文,以便close的時候能夠將資源徹底的釋放
	return db.QueryContext(context.Background(), query, args...)
}
//QueryContext方法
func (db *DB) QueryContext(ctx context.Context, query string, args ...interface{}) (*Rows, error) {
	var rows *Rows
	var err error
    // maxBadConnRetries是個靜態變量爲2,這裏最多會執行兩次從連接池中獲取連接,如果在兩次獲取
    // 過程中獲取到可用連接則直接返回
	for i := 0; i < maxBadConnRetries; i++ {
		rows, err = db.query(ctx, query, args, cachedOrNewConn)
		if err != driver.ErrBadConn {
			break
		}
	}
    // 如果兩次都獲取不到可用連接,則以請求獲取一個新連接的方式獲取並返回
	if err == driver.ErrBadConn {
		return db.query(ctx, query, args, alwaysNewConn)
	}
	return rows, err
}
// query方法如下
func (db *DB) query(ctx context.Context, query string, args []interface{}, strategy connReuseStrategy) (*Rows, error) {
    // 這裏是重點,這是真正申請連接的過程
	dc, err := db.conn(ctx, strategy)
	if err != nil {
		return nil, err
	}
    // 這裏是實際的查詢過程,不過多介紹
	return db.queryDC(ctx, nil, dc, dc.releaseConn, query, args)
}

真正根據策略申請連接的過程就在db.conn()方法中:

func (db *DB) conn(ctx context.Context, strategy connReuseStrategy) (*driverConn, error) {
	db.mu.Lock()
	if db.closed {
		db.mu.Unlock()
		return nil, errDBClosed
	}
	// 判斷context是否超時,因爲ctx可以設置有超時時間的也可以設置無超時時間的
	select {
	default:
	case <-ctx.Done():
		db.mu.Unlock()
		return nil, ctx.Err()
	}
	lifetime := db.maxLifetime

	// 嘗試獲取一個空閒連接
	numFree := len(db.freeConn)
	if strategy == cachedOrNewConn && numFree > 0 {
        // 取出第一個連接
		conn := db.freeConn[0]
        // copy是在原數組上覆蓋,所以需要最後尾數項
		copy(db.freeConn, db.freeConn[1:])
		db.freeConn = db.freeConn[:numFree-1]
        // 標記conn在使用
		conn.inUse = true
		db.mu.Unlock()
        // 如果conn達到超時時間,直接關閉連接,返回nil
		if conn.expired(lifetime) {
			conn.Close()
			return nil, driver.ErrBadConn
		}
		// 加鎖確保conn已經reset完畢
		conn.Lock()
		err := conn.lastErr
		conn.Unlock()
		if err == driver.ErrBadConn {
			conn.Close()
			return nil, driver.ErrBadConn
		}
		return conn, nil
	}

	// 如果實際連接數已經達到最大連接數,則將新到的請求阻塞,並存到db.connRequests(上面提到的)
	if db.maxOpen > 0 && db.numOpen >= db.maxOpen {
		// 創建一個req chan作爲線連接的緩存,暫存到db.connRequests中
		req := make(chan connRequest, 1)
        // nextRequestKeyLocked()實際就是去取db.nextRequest作爲key
		reqKey := db.nextRequestKeyLocked()
		db.connRequests[reqKey] = req
		db.mu.Unlock()

		// context超時處理
		select {
		case <-ctx.Done():
			// 如果超時保證新增的reqKey會被刪除掉
			db.mu.Lock()
			delete(db.connRequests, reqKey)
			db.mu.Unlock()
			select {
			default:
			case ret, ok := <-req:
				if ok {
					db.putConn(ret.conn, ret.err, false)
				}
			}
			return nil, ctx.Err()
            // 當req真正接到寫入的時候觸發下面的操作,其實和上面的邏輯類似
		case ret, ok := <-req:
			if !ok {
				return nil, errDBClosed
			}
			if ret.err == nil && ret.conn.expired(lifetime) {
				ret.conn.Close()
				return nil, driver.ErrBadConn
			}
			if ret.conn == nil {
				return nil, ret.err
			}
			// 加鎖確保conn已經reset完畢
			ret.conn.Lock()
			err := ret.conn.lastErr
			ret.conn.Unlock()
			if err == driver.ErrBadConn {
				ret.conn.Close()
				return nil, driver.ErrBadConn
			}
			return ret.conn, ret.err
		}
	}
    // 如果連接數沒有大於最大連接數時,進入以下邏輯
    // 先把當點連接數加1
	db.numOpen++ 
	db.mu.Unlock()
    // 獲取一個新連接,這裏也就是alwaysNewConn策略的實現
	ci, err := db.connector.Connect(ctx)
	if err != nil {
		db.mu.Lock()
        // 錯誤就回滾numOpen
		db.numOpen-- 
        // 很重要!!!下面說
		db.maybeOpenNewConnections()
		db.mu.Unlock()
		return nil, err
	}
	db.mu.Lock()
    // 構造成driverConn返回
	dc := &driverConn{
		db:        db,
		createdAt: nowFunc(),
		ci:        ci,
		inUse:     true,
	}
    // debug時會用到這個方法
	db.addDepLocked(dc, dc)
	db.mu.Unlock()
	return dc, nil
}

在這個方法的68行的case ret, ok := <-req:這部分,有人會很困惑,這個req是個空的chan,怎麼實行case裏面的代碼?connRequests中存的明明是個空的conn,之後怎麼用呢?答案在以下方法裏:

func (db *DB) maybeOpenNewConnections() {
    // 當和maxOpen比較還可以繼續創建連接時
    // 如果阻塞的連接過多,那麼也只能建立db.maxOpen - db.numOpen個
	numRequests := len(db.connRequests)
	if db.maxOpen > 0 {
		numCanOpen := db.maxOpen - db.numOpen
		if numRequests > numCanOpen {
			numRequests = numCanOpen
		}
	}
    // 循環numRequests次,給openerCh添加佔位量,異步goroutine接收到即可創建新的連接
	for numRequests > 0 {
		db.numOpen++ // optimistically
		numRequests--
		if db.closed {
			return
		}
		db.openerCh <- struct{}{}
	}
}

可能有人還是很蒙,這和connRequests這個map還是沒關係啊,這裏就關係到db.openerCh這個創建相關的chan如果處理了:

func (db *DB) openNewConnection(ctx context.Context) {
	ci, err := db.connector.Connect(ctx)
	db.mu.Lock()
	defer db.mu.Unlock()
	if db.closed {
		if err == nil {
			ci.Close()
		}
		db.numOpen--
		return
	}
	if err != nil {
		db.numOpen--
        // 關鍵點!!!
		db.putConnDBLocked(nil, err)
        // 這個就是嘗試db.openerCh <- struct{}{}這個操作
		db.maybeOpenNewConnections()
		return
	}
	dc := &driverConn{
		db:        db,
		createdAt: nowFunc(),
		ci:        ci,
	}
    // 關鍵點!!!創建好的dc先傳入了這個方法
	if db.putConnDBLocked(dc, err) {
		db.addDepLocked(dc, dc)
	} else {
		db.numOpen--
		ci.Close()
	}
}

看下db.putConnDBLocked(nil, err)這個方法:

func (db *DB) putConnDBLocked(dc *driverConn, err error) bool {
	if db.closed {
		return false
	}
	if db.maxOpen > 0 && db.numOpen > db.maxOpen {
		return false
	}
    // 這裏判斷了db.connRequests是否大於0,即是否有阻塞請求
	if c := len(db.connRequests); c > 0 {
		var req chan connRequest
		var reqKey uint64
        // 有阻塞請求,先delete掉一個key
		for reqKey, req = range db.connRequests {
			break
		}
		delete(db.connRequests, reqKey)
		if err == nil {
			dc.inUse = true
		}
        // 將剛申請好的dc包裝成connRequest賦給req
        // 這裏的req就是range db.connRequests返回的req
        // 這裏真正的獲取到了連接!!!
		req <- connRequest{
			conn: dc,
			err:  err,
		}
		return true
	} else if err == nil && !db.closed && db.maxIdleConnsLocked() > len(db.freeConn) {
        // 如果沒有阻塞的請求,將新建連接存入db.freeConn空閒連接池,並執行一次清理連接
		db.freeConn = append(db.freeConn, dc)
		db.startCleanerLocked()
		return true
	}
	return false
}

這就是整個連接池的運行原理。

附.go基礎包的數據庫連接池存在的問題

1.連接在用完會被清理session歸還連接池,如果當前連接數大於maxIdle,該連接會被專門的goroutine直接close掉,沒有延遲時間。如果配置不合理可能會造成連接不停的創建和銷燬。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章