Golang SQL連線池梳理

賜我白日夢發表於2020-07-31

一、如何理解資料庫連線

資料庫連線池是由客戶端維護的存放資料庫連線的池子,連線被維護在池子裡面,誰用誰來取,目的是降低頻繁的建立和關閉連線的開銷。

關於如何理解資料庫連線,大家可以藉助這個TCP程式設計的Demo來理解。

為了便於理解,可以MySQL-Server的連線池想象成就是這個簡單的Tcp-Server

func main() {
	// 1. 監聽埠 2.accept連線 3.開goroutine處理連線
	listen, err := net.Listen("tcp", "0.0.0.0:9090")
	if err != nil {
		fmt.Printf("error : %v", err)
		return
	}
	for{
		conn, err := listen.Accept()
		if err != nil {
			fmt.Printf("Fail listen.Accept : %v", err)
			continue
		}
		go ProcessConn(conn)
	}
}

// 處理網路請求
func ProcessConn(conn net.Conn) {
	// defer conn.Close()
	for  {
		bt,err:= coder.Decode(conn)
		if err != nil {
			fmt.Printf("Fail to decode error [%v]", err)
			return
		}
		s := string(bt)
		fmt.Printf("Read from conn:[%v]\n",s)
	}
}

對於我們現在看的sql包下的連線池,可以簡化認為它就是如下的tcp-client

conn, err := net.Dial("tcp", ":9090")
	defer conn.Close()
	if err != nil {
		fmt.Printf("error : %v", err)
		return
	}

	// 將資料編碼併傳送出去
	coder.Encode(conn,"hi server i am here");

	time.Sleep(time.Second*10

總體的思路可以認為,程式啟動的時候,根據我們的配置,sql包中的DB會為我們提前建立幾條這樣的conn,然後維護起來,不close()掉,我們想使用的時候問他拿即可。

至於為什麼是這個tcp的demo呢?因為資料庫連線的建立底層依賴的是tcp連線。基於tcp連線的基礎上實現客戶端和服務端資料的傳輸,再往上封裝一層mysql的握手、鑑權、互動協議對資料包進行解析、反解析,進而跑通整個流程。

二、連線池的工作原理

  • 連線池的建立
    • 後臺系統初始化時,連線池會根據系統的配置建立。
    • 但是在接受客戶端請求之前,並沒有真正的建立連線。
    • 在go語言中,先註冊驅動_ "github.com/go-sql-driver/mysql"
    • 初始化DB,呼叫Open函式,這時其實沒有真正的去獲取連線,而是去獲取DB操作的資料結構。
  • 連線池中連線的使用和管理
  • 連線池的關閉
    • 釋放連線
    • 關閉連線的請求佇列
    • connectionOpener(負責開啟連線的協程)
    • connectionResetter(重製連線狀態的協程)
    • connectionCleaner(定期清理過期連線的協程)

三、database/sql包結構

image-20200719230058101

driver/driver.go :定義了實現資料庫驅動所需要的介面,這些介面由sql包和具體的驅動包來實現

driver/types.go:定義了資料型別別名和轉換

convert:rows的scan

sql.go: 關於SQL資料庫的一些通用的介面、型別。包括:連線池、資料型別、連線、事物、statement

import "github.com/go-sql-driver/mysql” // 具體的驅動包
import "database/sql"

// 初始化連線
func initDB() (err error) {
	db, err = sql.Open("mysql", "root:root@tcp(127.0.0.1:3306)/test")
	if err != nil {
		panic(err)
	}
	// todo 不要在這裡關閉它, 函式一結束,defer就執行了
	// defer db.Close()
	err = db.Ping()
	if err != nil {
		return err
	}
	return nil
}

四、三個重要的結構體

4.1、DB

/**
	DB是代表零個或多個基礎連線池的資料庫控制程式碼。 對於多個goroutine併發使用是安全的。
	sql包會自動建立並釋放連線。 它還維護空閒連線的空閒池。 
	如果資料庫具有每個連線狀態的概念,則可以在事務(Tx)或連線(Conn)中可靠地觀察到這種狀態。
  呼叫DB.Begin之後,返回的Tx將繫結到單個連線。 
  在事務上呼叫Commit或Rollback後,該事務的連線將返回到DB的空閒連線池。
  池大小可以通過SetMaxIdleConns控制。
*/
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
	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
  
  // 連線池,在go中,連線的封裝結構體是:driverConn
	freeConn     []*driverConn
  
  // 連線請求的map, key是自增的int64型別的數,用於唯一標示這個請求分配的
	connRequests map[uint64]chan connRequest
  
  // 類似於binlog中的next trx_ix ,下一個事物的id
	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.
  // 他是個chan,用於通知connectionOpener()協程應該開啟新的連線了。
	openerCh          chan struct{}
  
  // 他是個chan,用於通知connectionResetter協程:重製連線的狀態。
	resetterCh        chan *driverConn
  
	closed            bool
  
  // 依賴,key是連線、statement
	dep               map[finalCloser]depSet
	lastPut           map[*driverConn]string // stacktrace of last conn's put; debug only
  
  // 連線池的大小,0意味著使用預設的大小2, 小於0表示不使用連線池
	maxIdle           int    // zero means defaultMaxIdleConns; negative means 0
  // 最大開啟的連線數,包含連線池中的連線和連線池之外的空閒連線, 0表示不做限制
	maxOpen           int    // <= 0 means unlimited
  
  // 連線被重用的時間,設定為0表示一直可以被重用。
	maxLifetime       time.Duration  // maximum amount of time a connection may be reused
  
  // 他是個chan,用於通知connectionCleaner協程去請求過期的連線
  // 當有設定最大存活時間時才會生效
	cleanerCh         chan struct{}
  
  // 等待的連線總數,當maxIdle為0時,waitCount也會一直為
  // 因為maxIdle為0,每一個請求過來都會開啟一條新的連線。
	waitCount         int64 // Total number of connections waited for.
  
  // 釋放連線時,因為連線池已滿而關閉的連線總數
  // 如果maxLifeTime沒有被設定,maxIdleClosed為0
	maxIdleClosed     int64 // Total number of connections closed due to idle.
  
  // 因為超過了最大連線時間,而被關閉的連線總數
	maxLifetimeClosed int64 // Total number of connections closed due to max free limit.
  
  // 當DB被關閉時,關閉connection opener和session resetter這兩個協程
	stop func() // stop cancels the connection opener and the session resetter.
}

4.2、driverConn

連線的封裝結構體:driverConn

// driverConn wraps a driver.Conn with a mutex, to
// be held during all calls into the Conn. (including any calls onto
// interfaces returned via that Conn, such as calls on Tx, Stmt,
// Result, Rows)
/**
	driverConn使用互斥鎖包裝Conn包裝
*/
type driverConn struct {
  // 持有對整個資料庫的抽象結構體
	db        *DB   		
	createdAt time.Time 

	sync.Mutex  // guards following
  
  // 對應於具體的連線,eg.mysqlConn
	ci          driver.Conn
  
  // 標記當前連線的狀態:當前連線是否已經關閉
	closed      bool
  // 標記當前連線的狀態:當前連線是否最終關閉,包裝 ci.Close has been called
	finalClosed bool // ci.Close has been called
  
  // 在這些連線上開啟的statement
	openStmt    map[*driverStmt]bool
  
  // connectionResetter返回的結果
	lastErr     error // lastError captures the result of the session resetter.

	// guarded by db.mu
  // 連線是否被佔用了
	inUse      bool
  
  // 在歸還連線時需要執行的程式碼。在noteUnusedDriverStatement中新增
	onPut      []func() // code (with db.mu held) run when conn is next returned
  
	dbmuClosed bool     // same as closed, but guarded by db.mu, for removeClosedStmtLocked
}

4.3、Conn

具體的連線: driver包下的Conn如下,是個介面,需要被具體的實現。

// Conn is assumed to be stateful.
type Conn interface {
	// Prepare returns a prepared statement, bound to this connection.
	Prepare(query string) (Stmt, error)

	// Close invalidates and potentially stops any current
	// prepared statements and transactions, marking this
	// connection as no longer in use.
	//
	// Because the sql package maintains a free pool of
	// connections and only calls Close when there's a surplus of
	// idle connections, it shouldn't be necessary for drivers to
	// do their own connection caching.
	Close() error

	// Begin starts and returns a new transaction.
	//
	// Deprecated: Drivers should implement ConnBeginTx instead (or additionally).
	Begin() (Tx, error)
}

五、流程梳理

5.1、先獲取DB例項

在golang中,要想獲取連線,一般我們都得通過下面這段程式碼獲取到DB的封裝結構體例項。

通過上面的三個結構體可以看出 DB 、driverConn、Conn的關係如下:

所以我們的程式碼一般長成下面這樣,先獲取一個DB結構體的例項,DB結果體中有維護連線池、以及和建立連線,關閉連線協程通訊的channel,已經各種配置引數。

上圖中淺藍色部分的 freeConn就是空閒連線池,裡面的driver包下的Conn interface就是具體的連線。

/**
 * MySQL連線相關的邏輯
 */
type Conenctor struct {
	BaseInfo BaseInfo
	DB       *sql.DB
}

func (c *Conenctor) Open() {
	// 讀取配置
	c.loadConfig()
	dataSource := c.BaseInfo.RootUserName + ":" + c.BaseInfo.RootPassword + "@tcp(" + c.BaseInfo.Addr + ":" + c.BaseInfo.Port + ")/" + c.BaseInfo.DBName
	db, Err := sql.Open("mysql", dataSource)
	if Err != nil {
		common.Error("Fail to opendb dataSource:[%v] Err:[%v]", dataSource, Err.Error())
		return
	}
	db.SetMaxOpenConns(500)
	db.SetMaxIdleConns(200)
	c.DB = db
	Err = db.Ping()
	if Err != nil {
		fmt.Printf("Fail to Ping DB Err :[%v]", Err.Error())
		return
	}
}

5.2、流程梳理入口:

比如我們自己寫程式碼時,可能會搞這樣一個方法做增刪改

// 插入、更新、刪除
func (c *Conenctor) Exec(ctx context.Context, 
                         sqlText string,
                         params ...interface{}) (qr *QueryResults) {
	qr = &QueryResults{}
	result, err := c.DB.ExecContext(ctx, sqlText, params...)
	defer HandleException()
	if err != nil {
		qr.EffectRow = 0
		qr.Err = err
		common.Error("Fail to exec qurey sqlText:[%v] params:[%v] err:[%v]", sqlText, params, err)
		return
	}
	qr.EffectRow, _ = result.RowsAffected()
	qr.LastInsertId, _ = result.LastInsertId()
	return
}

主要是使用DB.ExecContext()執行SQL,獲取返回值。

ctx是業務程式碼傳入的上線文,通常是做超時限制使用。

其實這裡並不是嚴格意義上的去執行sql,它其實是通過和MySQL-Server之間建立的連線將sql+params發往MySQL-Server去解析和執行。

進入DB.ExecContext()

主要邏輯如下:exec()方法的主要功能是:獲取連線,傳送sql和引數。

  • 如果獲取一次失敗一次,當失敗的次數達到sql包預定義的常量maxBadConnRetries的情況下,將會建立新的連線使用
  • 未超過maxBadConnRetries,被打上cachedOrNewConn,優先從空閒池中獲取連線
func (db *DB) ExecContext(ctx context.Context, query string, args ...interface{}) (Result, error) {
   var res Result
   var err error
   for i := 0; i < maxBadConnRetries; i++ {
      res, err = db.exec(ctx, query, args, cachedOrNewConn)
      if err != driver.ErrBadConn {
         break
      }
   }
   if err == driver.ErrBadConn {
      return db.exec(ctx, query, args, alwaysNewConn)
   }
   return res, err
}

跟進exec() --> db.conn(ctx, strategy)

func (db *DB) exec(ctx context.Context, query string, args []interface{}, strategy connReuseStrategy) (Result, error) {
  // 這個strategy就是上一步我們告訴他是建立新連線,還是優先從快取池中獲取連線。
	dc, err := db.conn(ctx, strategy)
  ..
}

5.3、獲取連線

跟進conn()方法

conn方法的返回值是driverConn,也就是我們上面說的資料庫連線,作用就是說,跟據傳遞進來的獲取策略,獲取資料庫連線,如果正常就返回獲取到的資料庫連線,異常就返回錯誤err

這張圖是conn獲取連線的流程圖,根據下面這段程式碼畫出來的,註釋有寫在程式碼上

// conn returns a newly-opened or cached *driverConn.
func (db *DB) conn(ctx context.Context, strategy connReuseStrategy) (*driverConn, error) {
	db.mu.Lock()
  // 先監測db是否關閉了
	if db.closed {
		db.mu.Unlock()
    // DB都關閉了,直接返回DBClosed錯誤,沒必要再去獲取連線。
		return nil, errDBClosed
	}
  // 檢查使用者傳遞進來的Context是否過期了
	select {
	default:
  // 如果使用者那邊使用了ctx.Done(),毫無疑問,會進入這個case中,返回Ctx錯誤  
	case <-ctx.Done():
		db.mu.Unlock()
		return nil, ctx.Err()
	}
  // 連線被重用的時間,如果為0,表示 理論上這個連線永不過期,一直可以被使用
	lifetime := db.maxLifetime

  // 看一下空閒連線池(他是個slice)是否是還有空閒的連線
	numFree := len(db.freeConn)
  // 如果獲取策略是優先從連線池中獲取,並且連線池中確實存在空閒的連線,就從freeConn中取連線使用。
	if strategy == cachedOrNewConn && numFree > 0 {
    // 假設空閒池還剩下五條連線:【1,2,3,4,5】
    // 取出第一條 conn == 1
		conn := db.freeConn[0]
    // 切片的拷貝,實現remove掉第一個連線的目的。
		copy(db.freeConn, db.freeConn[1:])
    // 如果db.freeConn[1:]會導致freeConn變小,所以這裡是 db.freeConn = db.freeConn[:numFree-1]
		db.freeConn = db.freeConn[:numFree-1]
    // 這裡獲取的連線是driverConn,它其實是對真實連線,driver.Conn的封裝。
    // 在driver.Conn的基礎上多一層封裝可以實現在driver.Conn的基礎上,加持上狀態資訊,如下
		conn.inUse = true
		db.mu.Unlock()
    // 檢查是否過期
		if conn.expired(lifetime) {
			conn.Close()
			return nil, driver.ErrBadConn
		}
		// Lock around reading lastErr to ensure the session resetter finished.
    // 加鎖處理,確保這個conn未曾被標記為 lastErr狀態。
    // 一旦被標記為這個狀態說明 ConnectionRestter協程在重置conn的狀態時發生了錯誤。也就是這個連線其實已經壞掉了,不可使用。
		conn.Lock()
		err := conn.lastErr
		conn.Unlock()
    // 如果檢測到這種錯誤,driver.ErrBadConn 表示連線不可用,關閉連線,返回錯誤。
		if err == driver.ErrBadConn {
			conn.Close()
			return nil, driver.ErrBadConn
		}
		return conn, nil
	}

	// Out of free connections or we were asked not to use one. If we're not
	// allowed to open any more connections, make a request and wait.
  // db.maxOpen > 0 表示當前DB例項允許開啟連線
  // db.numOpen >= db.maxOpen表示當前DB能開啟的連線數,已經大於它能開啟的最大連線數,就構建一個request,然後等待獲取連線
	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.
	
    // 構建connRequest這個channel,快取大小是1
    // 用於告訴connectionOpener協程,需要開啟一個新的連線。
		req := make(chan connRequest, 1)
    
    /**
      nextRequestKeyLocked函式如下:
     
      func (db *DB) nextRequestKeyLocked() uint64 {
				next := db.nextRequest
				db.nextRequest++
				return next
			}
			
			主要作用就是將nextRequest+1,
			至於這個nextRequest的作用我們前面也說過了,它相當於binlog中的next_trx下一個事物的事物id。
			言外之意是這個nextRequest遞增的(因為這段程式碼被加了lock)。
			看如下的程式碼中,將這個自增後的nextRequest當返回值返回出去。
			然後緊接著將它作為map的key
			
			至於這個map嘛:
		  在本文一開始的位置,我們介紹了DB結構體有這樣一個屬性,連線請求的map, key是自增的int64型別的數,
      用於唯一標示這個請求分配的
      connRequests map[uint64]chan connRequest 
     */
		reqKey := db.nextRequestKeyLocked()
    // 將這個第n個請求對應channel快取起來,開始等待有合適的機會分配給他連線
		db.connRequests[reqKey] = req
    // 等待數增加,解鎖
		db.waitCount++
		db.mu.Unlock()
    
		waitStart := time.Now()

		// Timeout the connection request with the context.
    // 進入下面的slice中
		select {
    // 如果客戶端傳入的上下文超時了,進入這個case
		case <-ctx.Done():
			// Remove the connection request and ensure no value has been sent
			// on it after removing.
      // 當上下文超時時,表示上層的客戶端程式碼想斷開,意味著在這個方法收到這個訊號後需要退出了
      // 這裡將db的connRequests中的reqKey清除,防止還給他分配一個連線。
			db.mu.Lock()
			delete(db.connRequests, reqKey)
			db.mu.Unlock()

			atomic.AddInt64(&db.waitDuration, int64(time.Since(waitStart)))
			// 這裡也會嘗試從req channel中獲取一下有沒有可用的連線
      // 如果有的話執行 db.putConn(ret.conn, ret.err, false) ,目的是釋放掉這個連線
			select {
			default:
			case ret, ok := <-req:
				if ok && ret.conn != nil {
          // 看到這裡只需要知道他是用來釋放連線的就ok,繼續往下看,稍後再殺回來
					db.putConn(ret.conn, ret.err, false)
				}
			}
      //返回ctx異常。
			return nil, ctx.Err()
    // 嘗試從 reqchannel 中取出連線
		case ret, ok := <-req:
			atomic.AddInt64(&db.waitDuration, int64(time.Since(waitStart)))
			// 處理錯誤
			if !ok {
				return nil, errDBClosed
			}
      // 檢測連線是否過期了,前面也提到過,DB例項有維護一個引數,maxLifeTime,0表示永不過期
			if ret.err == nil && ret.conn.expired(lifetime) {
				ret.conn.Close()
				return nil, driver.ErrBadConn
			}
      // 健壯性檢查
			if ret.conn == nil {
				return nil, ret.err
			}
      
			// Lock around reading lastErr to ensure the session resetter finished.
      // 檢查連線是否可用
			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
		}
	}
  // 程式碼能執行到這裡說明上面的if條件沒有被命中。
  // 換句話說,來到這裡說明具備如下條件
  // 1:當前DB例項的空閒連線池中已經沒有空閒連線了,獲取明確指定,不從空閒池中獲取連線,就想新建連線。
  // 2: 當前DB例項允許開啟連線
  // 3: DB例項目前開啟的連線數還沒有到達它能開啟的最大連線數的上限。
  
	// 記錄當前DB已經開啟的連線數+1
	db.numOpen++ // optimistically
	db.mu.Unlock()
	ci, err := db.connector.Connect(ctx)
	if err != nil {
		db.mu.Lock()
		db.numOpen-- // correct for earlier optimism
		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
}

5.4、釋放連線

連線被是過後是需要被釋放的

釋放連線的邏輯封裝在DB例項中

db.putConn(ret.conn, ret.err, false)

釋放連線的流程圖如下:

流程圖根據如下的程式碼畫出。

方法詳細資訊如下:

func (db *DB) putConn(dc *driverConn, err error, resetSession bool) {
  // 釋放連線的操作加鎖
	db.mu.Lock()
  // debug的資訊
	if !dc.inUse {
		if debugGetPut {
			fmt.Printf("putConn(%v) DUPLICATE was: %s\n\nPREVIOUS was: %s", dc, stack(), db.lastPut[dc])
		}
		panic("sql: connection returned that was never out")
	}
	if debugGetPut {
		db.lastPut[dc] = stack()
	}
  // 標記driverConn處理不可用的狀態
	dc.inUse = false

	for _, fn := range dc.onPut {
		fn()
	}
	dc.onPut = nil

  // 本方法的入參中有引數err
  // 當會話獲取出這個連線後,發現這個連線過期了、或者被標記上來lastErr時,再呼叫這個putConn方法時,同時會將這個錯誤傳遞進來,然後在這裡判斷,當出現壞掉的連線時就不直接把這個連線放回空閒連線池了。
	if err == driver.ErrBadConn {
		// Don't reuse bad connections.
		// Since the conn is considered bad and is being discarded, treat it
		// as closed. Don't decrement the open count here, finalClose will
		// take care of that.
    // 這個方法的作用如下:
    // 他會去判斷當前DB維護的map的容量,也就是前面提到的那種情況:當DB允許開啟連線,但是現在的連線數已經達到當前DB允許開啟的最大連線數上限了,那麼針對接下來想要獲取連線的請求的處理邏輯就是,構建一個req channel,放入connRequests這個map中,表示他們正在等待連線的建立。
    // 換句話說,這時系統時繁忙的,業務處於高峰,那麼問題來了,現在竟然出現了一個壞掉的連線,那為了把對業務線的影響降到最低,是不是得主動新建一個新的連線放到空閒連線池中呢?
    // 	db.maybeOpenNewConnections() 函式主要乾的就是這個事。
    // 	方法詳情如下
    /*
    	func (db *DB) maybeOpenNewConnections() {
					numRequests := len(db.connRequests)
					if db.maxOpen > 0 {
						numCanOpen := db.maxOpen - db.numOpen
					if numRequests > numCanOpen {
						numRequests = numCanOpen
					}
			}
			for numRequests > 0 {
						db.numOpen++ // optimistically
						numRequests--
						if db.closed {
							return
						}
				  // 它只是往這個	openerCh channel中寫入一個空的結構體,會有專門的協程負責建立連線
					db.openerCh <- struct{}{}
			}
		}
    */
		db.maybeOpenNewConnections()
    //  解鎖,關閉連線,返回
		db.mu.Unlock()
		dc.Close()
		return
	}
	if putConnHook != nil {
		putConnHook(db, dc)
	}
  // 如果DB已經關閉了,標記 resetSession為 false
	if db.closed {
		// Connections do not need to be reset if they will be closed.
		// Prevents writing to resetterCh after the DB has closed.
    // 當DB都已經關了,意味著DB裡面的連線池都沒有了,那當然不需要關閉連線池中的連線了~
		resetSession = false
	}
  // 如果DB沒有關閉的話,進入if程式碼塊
	if resetSession {
    // 將dricerConn中的Conn驗證轉換為driver.SessionResetter
		if _, resetSession = dc.ci.(driver.SessionResetter); resetSession {
      // 在此處鎖定driverConn,以便在連線重置之前不會釋放。
      // 必須在將連線放入池之前獲取鎖,以防止在重置之前將其取出
			dc.Lock()
		}
	}
  // 真正將連線放回空閒連線池中
  // 滿足connRequest或將driverConn放入空閒池並返回true或false
  /*
  	func (db *DB) putConnDBLocked(dc *driverConn, err error) bool {
  			// 檢測如果DB都關閉塊,直接返回flase
				if db.closed {
					return false
				}
				// 如果DB當前開啟的連線數大於DB能開啟的最大的連線數,返回false
				if db.maxOpen > 0 && db.numOpen > db.maxOpen {
					return false
				}
				//如果等待獲取連線的map中有存貨
		 if c := len(db.connRequests); c > 0 {
		 		
				var req chan connRequest
				var reqKey uint64
				// 取出map中的第一個key
				for reqKey, req = range db.connRequests {
					break
				}
				// 將這個key,value再map中刪除
				delete(db.connRequests, reqKey) // Remove from pending requests.
				// 重新標記這個連線是可用的狀態
				if err == nil {
					dc.inUse = true
				}
				// 將這個連線放入到 req channel中,給等待連線到會話使用
				req <- connRequest{
					conn: dc,
					err:  err,
				}
				return true
				
		// 來到這個if,說明此時沒有任何請求在等待獲取連線,並且沒有發生錯誤,DB也沒有關閉
		} else if err == nil && !db.closed {
				// 比較當前空閒連線池的大小(預設是2) 和 freeConn空閒連線數的數量
				// 意思是,如果空閒的連線超出了這個規定的閾值,空閒連線是需要被收回的。
				if db.maxIdleConnsLocked() > len(db.freeConn) {
				  // 收回
					db.freeConn = append(db.freeConn, dc)
					db.startCleanerLocked()
					return true
				}
				// 如果空閒連線還沒到閾值,保留這個連線當作空閒連線
				db.maxIdleClosed++
		}		
				// 收回空閒連線返回false
				return false
}
  */
  
  // 如果將連線成功放入了空閒連線池,或者將連線成功給了等待連線到會話使用,此處返回true
  // 收回空閒連線返回false
  // 程式碼詳情就是在上面的這段註釋中
	added := db.putConnDBLocked(dc, nil)
	db.mu.Unlock()
	
  // 如果
	if !added {
    // 如果DB沒有關閉,進入if
		if resetSession {
			dc.Unlock()
		}
		dc.Close()
		return
	}
  // 重新校驗,如果連線關閉了,進入if
	if !resetSession {
		return
	}
  
  // 如果負責重置 conn狀態的執行緒阻塞住了,那麼標記這個driverConn為lastErr
	select {
	default:
		// If the resetterCh is blocking then mark the connection
		// as bad and continue on.
		dc.lastErr = driver.ErrBadConn
		dc.Unlock()
	case db.resetterCh <- dc:
	}
}

5.5、connectionOpener

5.5.1、是什麼?

這個connectionOpener是一個工作協程,它會去嘗試消費指定的channel,負責建立資料庫連線,其實在前面閱讀獲取連線的邏輯時,有這樣的兩種情況會阻塞等待connectionOpener來新建立連線:

第一種:當獲取連線的策略是優先從cache連線池中獲取出來,但是空閒連線池已經沒有空閒的連線了,首先這時DB允許開啟連線,但是DB能開啟的連線數已經達到了它能開啟的連線數的上線,所以得等待有空閒連線出現,或者等有連線被釋放後,DB能當前開啟的連線數小於了它能開啟的連線數的最大值,這時它會被阻塞等待去嘗試建立連線。

第二種:獲取連線的策略不再是優先從空閒緩衝池中獲取連線,直接明瞭的想獲取最一條新連線,同樣的此時DB已經開啟的連線數大於它能開啟連線數的上線,它會被阻塞等待建立連線。

image-20200731221533203

5.5.2、什麼時候開啟的?
func OpenDB(c driver.Connector) *DB {
	ctx, cancel := context.WithCancel(context.Background())
	db := &DB{
		connector:    c,
		openerCh:     make(chan struct{}, connectionRequestQueueSize),
		resetterCh:   make(chan *driverConn, 50),
		lastPut:      make(map[*driverConn]string),
		connRequests: make(map[uint64]chan connRequest),
		stop:         cancel,
	}
	// 可以看到他是在DB被例項化時開啟的。
	go db.connectionOpener(ctx)
	go db.connectionResetter(ctx)

	return db
}
5.5.3、程式碼詳情

可以看到它一直嘗試從db的openerCh中獲取內容,而且只要獲取到了內容,就會呼叫方法開啟連線。

// Runs in a separate goroutine, opens new connections when requested.
func (db *DB) connectionOpener(ctx context.Context) {
	for {
		select {
		case <-ctx.Done():
			return
    // here  
		case <-db.openerCh:
			db.openNewConnection(ctx)
		}
	}
}
5.5.4、誰往openerCh中投放訊息?

往channl中投放訊息的邏輯在db的mayBeOpenNewConnections中

func (db *DB) maybeOpenNewConnections() {
  // 通過檢查這個map的長度來決定是否往opennerCh中投放訊息
	numRequests := len(db.connRequests)
	if db.maxOpen > 0 {
		numCanOpen := db.maxOpen - db.numOpen
		if numRequests > numCanOpen {
			numRequests = numCanOpen
		}
	}
	for numRequests > 0 {
		db.numOpen++ // optimistically
		numRequests--
		if db.closed {
			return
		}
    // 一旦執行了這一步,connectionOpener 就會監聽到去建立連線。
		db.openerCh <- struct{}{}
	}
}
5.5.5、注意點:

在DB結構體中有這樣一個屬性

  // 連線池的大小,0意味著使用預設的大小2, 小於0表示不使用連線池
	maxIdle           int    // zero means defaultMaxIdleConns; negative means 0

表示空閒連線池預設的大小,如果它為0,表示都沒有快取池,也就意味著會為所有想獲取連線的請求建立新的conn,這是也就不會有這個opnerCh,更不會有connectionOpener

5.6、connectionCleaner

5.6.1、是什麼?有啥用?

它同樣以一條協程的形式存在,用於定時清理資料庫連線池中過期的連線

func (db *DB) startCleanerLocked() {
	if db.maxLifetime > 0 && db.numOpen > 0 && db.cleanerCh == nil {
		db.cleanerCh = make(chan struct{}, 1)
		go db.connectionCleaner(db.maxLifetime)
	}
}
5.6.2、注意點

同樣的,DB中存在一個引數:maxLifetime

它表示資料庫連線最大的生命時長,如果將它設定為0,表示這個連線永不過期,既然所有的連線永不過期,就不會存在connectionCleaner去定時根據maxLifetime 來定時清理連線。

它的呼叫時機是:需要將連線放回到連線池時呼叫。

5.7、connectionRestter

5.7.1、作用

我們使用獲取的連線的封裝結構體是driverConn,其實它是會driver包下的Conn連線的又一層封裝,目的是增強

driver包下的Conn的,多出來了一些狀態。當將使用完畢的連線放入連線池時,就得將這些狀態清除掉。

使用誰去清除呢?就是這個go 協程:connectionRestter

當connectionRestter碰到錯誤時,會將這個conn標記為lastErr,連線使用者在使用連線時會先校驗conn的諸多狀態,比如出現lastErr,會返回給客戶端 badConnErr

六、MySQL連線池所受的限制

資料庫連線池大大小到底設定為多少,得根據業務流量已經資料庫所在機器的效能綜合考慮。

mysql連線數到配置在 my.cnf中,具體的引數是max_connections。

當業務流量異常猛烈時,很可能會出現這個問題:to many connections

對於操縱系統核心來說,當他接受到一個tcp請求就會在本地建立一個由檔案系統管理的socket檔案。在linux中我們將它叫做檔案控制程式碼。

linux為防止單一程式將系統資源全部耗費掉,會限制程式最大能開啟的連線數為1024,這意味著,哪怕通過改配置檔案,將mysql能開啟的連線池設定為9999,事實上它能開啟的檔案數最多不會超過1024。

這個問題也好解決:

命令:設定單個程式能開啟的最大連線數為65535

ulimit -HSn 65535

通過命令: 檢視程式被限制的使用各種資源的量

ulimit -a 

core file size: 程式崩潰是轉儲檔案大小限制
man loaded memort 最大鎖定記憶體大小
open file 能開啟的檔案控制程式碼數

這些變數定義在 /etc/security/limits.conf配置檔案中。

七、關於失效的連線

情況1: 客戶端主動斷開

如果是客戶端主動將連線close(), 那往合格連線中寫資料時會得到ErrBadConn的錯誤,如果此時依然可以重試,將會獲取新的連線。

程式碼如下:

func (db *DB) ExecContext(ctx context.Context, query string, args ...interface{}) (Result, error) {
	var res Result
	var err error
	for i := 0; i < maxBadConnRetries; i++ {
		res, err = db.exec(ctx, query, args, cachedOrNewConn)
		if err != driver.ErrBadConn {
			break
		}
	}
	if err == driver.ErrBadConn {
		return db.exec(ctx, query, args, alwaysNewConn)
	}
	return res, err
}

情況2: 服務端掛啦

因為這種資料庫連線底層使用的是tcp實現。(tcp本身是支援全雙工的,客戶端和服務端支援同時往對方傳送資料)依賴諸如:校驗和、確認應答和序列號機制、超時重傳、連線管理(3次握手,4次揮手)、以及滑動視窗、流量控制、擁賽避免去實現整個資料互動的可靠性,協調。

這時客戶端拿著一條自認為是正常的連線,往連線裡面寫資料。然鵝,另一端端服務端已經掛了~,但是不幸的是,客戶端的tcp連線根本感知不到~~~。

但是當它去讀取服務端的返回資料時會遇到錯誤:unexceptBadConn EOF

八、連線的有效性

  • 思路1:

設定連線的屬性: maxLifeTime

上面也說過了,當設定了這個屬性後,DB會開啟一條協程connectionCleaner,專門負責清理過期的連線。

這在一定程度上避免了服務端將連線斷掉後,客戶端無感知的情況。

maxLifeTime的值到底設定多大?參考值,比資料庫的wait_timeout小一些就ok。

  • 思路2:

主動檢查連線的有效性。

比如在連線放回到空閒連線池前ping測試。在使用連線傳送資料前進行連通性測試。

相關文章