golangデータベース接続プールデータベース/ sql実現の原理の分析

Golangのデータベースへの要求は、一連のユニバーサル接続プールを抽象化します。goメカニズムを使用すると、golangはドライバーインターフェイスを提供するだけで済み、基盤となるさまざまなデータベースプロトコルは、ユーザーが独自のデータベースに従って駆動します。できる。

この記事では、ソースコードの実装の観点から、ここでの詳細と回避する必要のある落とし穴について説明します。1.14コード分析に基づいて、1.15でいくつかのバグが修正または最適化されました。これについても、ここで説明します。

golangバージョン:1.14

ディレクトリ構造の説明

└── sql
    ├── convert.go           # 结果行的读取与转换
    ├── convert_test.go
    ├── ctxutil.go           # 绑定上下文的一些通用方法
    ├── doc.txt
    ├── driver               # driver 定义来实现数据库驱动所需要的接口
    │   ├── driver.go
    │   ├── types.go         # 数据类型别名和转换
    │   └── types_test.go
    ├── example_cli_test.go
    ├── example_service_test.go
    ├── example_test.go
    ├── fakedb_test.go
    ├── sql.go               # 通用的接口和类型,包括事物,连接等
    └── sql_test.go

主なデータ構造

1. sql.DB

type DB struct {
    // Atomic access only. At top of struct to prevent mis-alignment
    // on 32-bit platforms. Of type time.Duration.
    waitDuration int64          // 等待新的连接所需要的总时间
    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 // map[一级对象]map[二级对象]bool,一个外部以来,用于自动关闭
    lastPut           map[*driverConn]string // stacktrace of last conn's put; debug only
    maxIdle           int                    // zero means defaultMaxIdleConns(2); negative means 0
    maxOpen           int                    // <= 0 means unlimited
    maxLifetime       time.Duration          // maximum amount of time a connection may be reused
    cleanerCh         chan struct{}          // 用于通知清理过期的连接,maxlife时间改变或者连接被关闭时会通过该channel通知
    waitCount         int64 // Total number of connections waited for.   // 这些状态数据,可以通过db.Stat() 获取
    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.
}

sql.DBは接続ではなく、データベースの抽象インターフェイスであり、接続プール全体へのハンドルです。複数のゴルーチンに対して同時に安全です。ドライバーに応じてデータベース接続を開閉したり、接続プールを管理したりできます。これは、さまざまなデータベースで同じです。

2. sql.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)
type driverConn struct {
   db        *DB
   createdAt time.Time

   sync.Mutex  // guards following
   ci          driver.Conn  // 由不同的驱动自己实现,对应一条具体的数据库连接
   needReset   bool         // The connection session should be reset before use if true.
   closed      bool         // 当前连接的状态,是否已经关闭
   finalClosed bool         // ci.Close has been called
   openStmt    map[*driverStmt]bool

   // guarded by db.mu
   inUse      bool
   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
}

実際のデータベース接続や関連するステータス情報などを含む、単一の接続のカプセル化。

3. driver.Conn

// Conn is a connection to a database. It is not used concurrently
// by multiple goroutines.
//
// 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)
}

特定のデータベース接続は、さまざまなドライバー自体によって実装される必要があります

4. driver.Driver

type Driver interface {
    Open(name string) (Conn, error)
}

ドライバには関数が1つだけ含まれ、Open()は使用可能な接続を返すために使用されます。これは、新しく確立された接続、または以前にキャッシュされた閉じられた接続の場合があります。

5. driver.DriverContext

type DriverContext interface {
// OpenConnector must parse the name in the same format that Driver.Open
// parses the name parameter.
    OpenConnector(name string) (Connector, error)
}

DriverContextの目的は、drievrコンテキスト情報を維持し、新しい接続が作成されるたびにdsnを解析する必要をなくすことです。Driverオブジェクトはそれ自体で実装する必要があります。

6. driver.Connector

type Connector interface {
// Connect returns a connection to the database.
// Connect may return a cached connection (one previously
// closed), but doing so is unnecessary; the sql package
// maintains a pool of idle connections for efficient re-use.
//
// The provided context.Context is for dialing purposes only
// (see net.DialContext) and should not be stored or used for
// other purposes.
//
// The returned connection is only used by one goroutine at a
// time.
    Connect(context.Context) (Conn, error)
// Driver returns the underlying Driver of the Connector,
// mainly to maintain compatibility with the Driver method
// on sql.DB.
    Driver() Driver
}

driver.Connectorは、さまざまなタイプのデータベースによって実装される、インターフェイスタイプのオブジェクトであるドライバーのソケットです。
driver.Connectorには2つの機能が含まれています。

  • 接続は接続を確立するために使用されます
  • Driverは、Driverオブジェクトを返すために使用されます。Driverはインターフェイスタイプのオブジェクトでもあり、さまざまなデータベースで実装する必要があります。

主な操作プロセス

1.ドライバーを登録します

import (
    _ "github.com/go-sql-driver/mysql"
)

var (
    driversMu sync.RWMutex
    drivers   = make(map[string]driver.Driver)
)
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[name] = driver
}

/ database / sqlは、一般的なデータベース接続プールを提供します。異なるデータベースに接続する場合、対応するデータベースドライバーを登録するだけで使用できます。

ここでの登録は、実際にはデータベース名と対応するデータベースドライバ(データベース接続ラッパー)をマップに追加することです。インポートされた各ライブラリは、init関数の登録関数を呼び出して実装する必要があります。

2.接続プールハンドルsql.Open()を作成します

func Open(driverName, dataSourceName string) (*DB, error) {
    driversMu.RLock()
    driveri, ok := drivers[driverName]  // 1
    driversMu.RUnlock()
    if !ok {
        return nil, fmt.Errorf("sql: unknown driver %q (forgotten import?)", driverName)
    }
    if driverCtx, ok := driveri.(driver.DriverContext); ok {  // 2
        connector, err := driverCtx.OpenConnector(dataSourceName)
        if err != nil {
            return nil, err
        }
        return OpenDB(connector), nil  // 3
    }
    return OpenDB(dsnConnector{dsn: dataSourceName, driver: driveri}), nil  // 4
}

func OpenDB(c driver.Connector) *DB {
   ctx, cancel := context.WithCancel(context.Background())
   db := &DB{
      connector:    c,
      openerCh:     make(chan struct{}, connectionRequestQueueSize),
      lastPut:      make(map[*driverConn]string),
      connRequests: make(map[uint64]chan connRequest),
      stop:         cancel,
   }

   go db.connectionOpener(ctx)  // 通过channel通知来创建连接
   // go db.connectionResetter(ctx) // 用于重置连接,1.14废弃
   return db
}

Open関数は通常、dbの初期化として解釈されます。ここでは、対応するドライバーがドライバー名から取得され、ドライバーに対して一連の初期化操作が実行されます。Openはdbとの接続を確立せず、これらのデータ構造のみを操作することに注意してください。バックグラウンドコロチンなどのアクションを開始します。

ここでのdataSourceNameはdsnと呼ばれ、データベースへの接続に必要なパラメーター、ユーザー名、パスワード、IPポート、およびその他の情報が含まれています。さまざまなドライバーが独自に分析を実装します。もちろん、一部のドライバーは、autocommitなどのdsnでのデータベースパラメーターの構成もサポートします。文字列を解析して得られる情報は一定量のリソースを消費するため、解析結果をキャッシュする機能も備えており、新しい接続が確立されるたびに解析する必要がありません。これを行うには、駆動する必要があります。 driver.DriverContextインターフェイス。

現時点ではそのような構造がありますが、現時点では接続プールに接続がありません。つまり、dbへの実際のアクセスはありません。

golangデータベース接続プールデータベース/ sql実現の原理の分析

3.データベース接続パラメーターを設定します

アイドル接続の最大数。アイドル接続の数がこの値を超えると、それらは閉じられます。デフォルトはdefaultMaxIdleConns(2)です。

func (db *DB) SetMaxIdleConns(n int) {} 

許可されているオープン接続の最大数この数を超えると、新しい接続を確立することはできず、作業中の連絡先は接続の解放を待つことのみをブロックできます。

func (db *DB) SetMaxOpenConns(n int) {}

接続を再利用できる最大時間、つまり、接続が閉じられる時間ですが、現在の要求が完了するのを待ってから閉じ、遅延して閉じます。これは非常に味のないパラメータです。

func (db *DB) SetConnMaxLifetime(d time.Duration) {
    // 通过启动一个单独的协程 connectionCleaner 来实现 
    startCleanerLocked {
        go db.connectionCleaner(db.shortestIdleTimeLocked())
    }
}

1.15以降、接続の最大アイドル時間である新しいパラメーターが追加されます。アイドル時間がこの値を超えるとアイドル時間は閉じられますが、現在の要求が完了した後に閉じられ、遅延して閉じられます。

func (db *DB) SetConnMaxIdleTime(d time.Duration) {
    // 1.15 实现了对空闲连接的超时回收,复用了SetConnMaxLifetime的部分逻辑,也是在connectionCleaner协程中实现的
}

SetConnMaxLifetimeおよびSetConnMaxIdleTimeの詳細な実装

  • 1.14実装
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)
   }
}

func (db *DB) connectionCleaner(d time.Duration) {
   const minInterval = time.Second

   if d < minInterval {
      d = minInterval
   }
   t := time.NewTimer(d)

   for {
      // 当maxlife时间到达
      // 或者maxlife发生改变及db被close
      select {
      case <-t.C:
      case <-db.cleanerCh: // maxLifetime was changed or db was closed.
      }

      db.mu.Lock()
      d = db.maxLifetime
      if db.closed || db.numOpen == 0 || d <= 0 {
         db.cleanerCh = nil
         db.mu.Unlock()
         return
      }

      // 循环处理free状态的连接
      expiredSince := nowFunc().Add(-d)
      var closing []*driverConn
      for i := 0; i < len(db.freeConn); i++ {
         c := db.freeConn[i]
         if c.createdAt.Before(expiredSince) {
            closing = append(closing, c)
            last := len(db.freeConn) - 1
            db.freeConn[i] = db.freeConn[last]
            db.freeConn[last] = nil
            db.freeConn = db.freeConn[:last]
            i--
         }
      }
      db.maxLifetimeClosed += int64(len(closing))
      db.mu.Unlock()

      for _, c := range closing {
         c.Close()
      }

      // 如果maxlife被重置,需要更新定时器时间
      if d < minInterval {
         d = minInterval
      }
      t.Reset(d)
   }
}
  • 1.15実装
func (db *DB) startCleanerLocked() {
  if (db.maxLifetime > 0 || db.maxIdleTime > 0) && db.numOpen > 0 && db.cleanerCh == nil {
    db.cleanerCh = make(chan struct{}, 1)
    go db.connectionCleaner(db.shortestIdleTimeLocked())  // maxidle和maxlife取较小值
  }
}

func (db *DB) connectionCleaner(d time.Duration) {
  const minInterval = time.Second

  if d < minInterval {
    d = minInterval
  }
  t := time.NewTimer(d)

  for {
    select {
    case <-t.C:
    case <-db.cleanerCh: // maxLifetime was changed or db was closed.
    }

    db.mu.Lock()
    d = db.shortestIdleTimeLocked()
    if db.closed || db.numOpen == 0 || d <= 0 {
      db.cleanerCh = nil
      db.mu.Unlock()
      return
    }

    closing := db.connectionCleanerRunLocked()
    db.mu.Unlock()
    for _, c := range closing {
      c.Close()
    }

    if d < minInterval {
      d = minInterval
    }
    t.Reset(d)
  }
}

// 对idle超时和life超时的连接分别收集,统一返回
func (db *DB) connectionCleanerRunLocked() (closing []*driverConn) {
  if db.maxLifetime > 0 {
    expiredSince := nowFunc().Add(-db.maxLifetime)
    for i := 0; i < len(db.freeConn); i++ {
      c := db.freeConn[i]
      if c.createdAt.Before(expiredSince) {
        closing = append(closing, c)
        last := len(db.freeConn) - 1
        db.freeConn[i] = db.freeConn[last]
        db.freeConn[last] = nil
        db.freeConn = db.freeConn[:last]
        i--
      }
    }
    db.maxLifetimeClosed += int64(len(closing))
  }

  if db.maxIdleTime > 0 {
    expiredSince := nowFunc().Add(-db.maxIdleTime)
    var expiredCount int64
    for i := 0; i < len(db.freeConn); i++ {
      c := db.freeConn[i]
      if db.maxIdleTime > 0 && c.returnedAt.Before(expiredSince) {
        closing = append(closing, c)
        expiredCount++
        last := len(db.freeConn) - 1
        db.freeConn[i] = db.freeConn[last]
        db.freeConn[last] = nil
        db.freeConn = db.freeConn[:last]
        i--
      }
    }
    db.maxIdleTimeClosed += expiredCount
  }
  return
}

1.14と1.15の実装ロジックは基本的に同じですが、アイドルタイムアウトの判断との互換性が追加されます

4.データベースにアクセスします

上記の初期化アクションを実行した後、習慣に従って、通常はデータベースに接続して、ユーザー名とパスワードが正しいかどうかなどの接続パラメーターが正常かどうかを判断しようとしますが、ユーザー要求は送信しません。一般的なアプローチは、 db.Ping()、

func (db *DB) Ping() error {
   return db.PingContext(context.Background())
}

func (db *DB) PingContext(ctx context.Context) error {
   var dc *driverConn
   var err error

   // 获取一个可用连接,后面会看到一样的逻辑,这里先跳过细节
   for i := 0; i < maxBadConnRetries; i++ {
      dc, err = db.conn(ctx, cachedOrNewConn)
      if err != driver.ErrBadConn {
         break
      }
   }
   if err == driver.ErrBadConn {
      dc, err = db.conn(ctx, alwaysNewConn)  // db.conn 是来获取可用连接的,是数据库连接池较为核心的一部分
   }
   if err != nil {
      return err
   }

   // 发送ping命令
   return db.pingDC(ctx, dc, dc.releaseConn)
}

func (db *DB) pingDC(ctx context.Context, dc *driverConn, release func(error)) error {
   var err error
   if pinger, ok := dc.ci.(driver.Pinger); ok {
      withLock(dc, func() {
         err = pinger.Ping(ctx)  // 这里需要驱动自己去实现,对应mysql来说,发送的是sql_type=14(COM_PING)的请求包
      })
   }
   release(err)   // 将该连接放回到free池
   return err
}

5.sqlリクエストを送信します

sqlを送信する最も簡単な方法のいくつかを次に示します

// 没有结果集,值返回ok/error包
func (db *DB) Exec(query string, args ...interface{}) (Result, error) {}
func (db *DB) ExecContext(ctx context.Context, query string, args ...interface{}) (Result, error) {}

// 返回大于0条结果集
func (db *DB) Query(query string, args ...interface{}) (*Rows, error) {}
func (db *DB) QueryContext(ctx context.Context, query string, args ...interface{}) (*Rows, error) {}

// 预期结果集只有一行,没有结果集Scan时报ErrNoRows,Scan结果如果有多行,只取第一行,多余的数据行丢弃
func (db *DB) QueryRow(query string, args ...interface{}) *Row {}
func (db *DB) QueryRowContext(ctx context.Context, query string, args ...interface{}) *Row {}

ここにいくつかのメモがあります:

  • 各メソッドには、同時にContextサフィックスが付いた別のメソッドがあることがわかります。呼び出し関係を見ると、Contextのない関数(Exec / Query / QueryRow)は、実際にはContextのある関数(ExecContext /)であることがわかります。 QueryContext / QueryRowContext)、ここでのコンテキストは、タイムアウト制限などの信号同期に使用されるほとんどのライブラリ関数と同じであり、通常、個別に設定する必要はありません。
  • 各関数パラメーターが可変パラメーターリストをサポートしていることがわかります。使用法はprepareの使用法と同じです。使用?プレースホルダーとして、SQLを直接スペルするか、プレースホルダーを使用します。どちらが良いですか?
    rows1, err := db.Query("select * from t1 where a = 1”)
    rows2, err := db.Query("select * from t1 where a = ?", 1)

これら2つのSQL実行の結果は同じですが、最下層が異なり、異なるドライバーの特定の実装とはわずかに異なります。

mysqlを例にとると、違いは、最初のクエリが実際に1つのsql(sql_type:3)を送信し、2番目のクエリが実際に2つのsql(sql_type:22とsql_tyep:23)を送信し、最初に準備してから実行することです。バイナリプロトコルの方が高速ですが、毎回2つのsqlを送信します。最初に送信される準備は1回だけ実行され、準備情報をアクティブに回復しません。

このインターフェースの設計の開始時に、prepare + executeのアイデアに従って設計する必要があります。プレースホルダーパラメーターの数が0の場合、最適化してsqlを直接送信できるかどうかは、基盤となるドライバーインターフェースがそれをサポートしているかどうか、つまり、prepare + executeに依存します。

次に、クエリを例として取り上げて、特定の実装プロセスを確認します

func (db *DB) Query(query string, args ...interface{}) (*Rows, error) {
   return db.QueryContext(context.Background(), query, args...)
}

func (db *DB) QueryContext(ctx context.Context, query string, args ...interface{}) (*Rows, error) {
   var rows *Rows
   var err error

   // 执行query,优先从连接池获取连接,如果获取到badconn(以及关闭的连接),重试,最多重试maxBadConnRetries(2)次
   for i := 0; i < maxBadConnRetries; i++ {
      rows, err = db.query(ctx, query, args, cachedOrNewConn)
      if err != driver.ErrBadConn {
         break
      }
   }

   // 一定创建新的连接执行query
   if err == driver.ErrBadConn {
      return db.query(ctx, query, args, alwaysNewConn)
   }
   return rows, err
}

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)
}

通常のSQLを実行するには2つのステップが必要であることがわかります。最初のステップは接続を取得すること(db.conn)であり、2番目のステップはクエリを実行することです(db.queryDC)。

6.接続を取得します

//接続を取得するための2つの戦略、alwaysNewConnとcachedOrNewConnを提供します。文字通り、常に新しいものを作成し、無料の接続の再利用を優先します

func (db *DB) conn(ctx context.Context, strategy connReuseStrategy) (*driverConn, error) {
   // 全局加锁 这里有个连接池的大锁,需要注意
   db.mu.Lock()
   if db.closed {
      db.mu.Unlock()
      return nil, errDBClosed
   }

   // context 超时检测
   select {
   default:
   case <-ctx.Done():
      db.mu.Unlock()
      return nil, ctx.Err()
   }
   lifetime := db.maxLifetime

   // 优先从free池中获取连接
   numFree := len(db.freeConn)
   if strategy == cachedOrNewConn && numFree > 0 {
      // 取第一个free连接
      conn := db.freeConn[0]
      // 切片拷贝
      copy(db.freeConn, db.freeConn[1:])
      // 调整切片长度
      db.freeConn = db.freeConn[:numFree-1]
      conn.inUse = true
      db.mu.Unlock()
      // 检查连接是否超时,超时则返回错误
      if conn.expired(lifetime) {
         conn.Close()
         return nil, driver.ErrBadConn
      }

      // 对连接状态进行重置,通常是使用过的连接需要重置,避免连接已经处于不可用状态
      if err := conn.resetSession(ctx); err == driver.ErrBadConn {
         conn.Close()
         return nil, driver.ErrBadConn
      }
      return conn, nil
   }

   // 已经没有free连接,或者策略要求创建一个新连接

   // 当前打开的连接已经达到了允许打开连接数的上限,需要阻塞等待
   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.

      // 建立一个唯一key和请求连接connRequest channel的映射
      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 {
      // 如果超时,从map中删除该key,记录统计信息,并检查连接是否已经就绪
      case <-ctx.Done():
         // Remove the connection request and ensure no value has been sent
         // on it after removing.
         db.mu.Lock()
         delete(db.connRequests, reqKey)
         db.mu.Unlock()
         atomic.AddInt64(&db.waitDuration, int64(time.Since(waitStart)))
         // 如果已经生成了可用连接,将新连接放回到free池中
         select {
         default:
         case ret, ok := <-req:
            if ok && ret.conn != nil {
               db.putConn(ret.conn, ret.err, false)
            }
         }
         return nil, ctx.Err()

      case ret, ok := <-req:
         atomic.AddInt64(&db.waitDuration, int64(time.Since(waitStart)))

         if !ok {
            return nil, errDBClosed
         }
         // Only check if the connection is expired if the strategy is cachedOrNewConns.
         // If we require a new connection, just re-use the connection without looking
         // at the expiry time. If it is expired, it will be checked when it is placed
         // back into the connection pool.
         // This prioritizes giving a valid connection to a client over the exact connection
         // lifetime, which could expire exactly after this point anyway.
         // 对cachedOrNewConn策略的连接请求,需要判断连接是否过期
         // 如果是请求新连接,则不做判断,等连接被放回free池中时再回收
         if strategy == cachedOrNewConn && ret.err == nil && ret.conn.expired(lifetime) {
            ret.conn.Close()
            return nil, driver.ErrBadConn
         }
         if ret.conn == nil {
            return nil, ret.err
         }

         // Reset the session if required.
         if err := ret.conn.resetSession(ctx); err == driver.ErrBadConn {
            ret.conn.Close()
            return nil, driver.ErrBadConn
         }
         return ret.conn, ret.err
      }
   }

   // 由于未达到连接数上限,直接创建新连接
   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
}

要約すると、接続プールへの接続を申請すると、

  • 戦略がcachedOrNewConnであり、空き接続プールにある場合、それは直接取り出されます。
  • 接続プールにアイドル状態の接続がない場合、またはポリシーがalwaysNewConnであり、現在の接続が上限を超えていない場合は、直接作成されます。
  • それ以外の場合、チャネルは非同期で作成および確立するために使用され、呼び出しサイトは接続の待機をブロックします。

7.クエリを実行します

クエリ

// ctx 是调用sql设置的上下文
// txctx 是事务的上下文,如果有
// releaseConn 上层传递的函数句柄,连接使用完后,将该连接放回到连接池

func (db *DB) queryDC(ctx, txctx context.Context, dc *driverConn, releaseConn func(error), query string, args []interface{}) (*Rows, error) {
   queryerCtx, ok := dc.ci.(driver.QueryerContext)
   var queryer driver.Queryer
   if !ok {
      queryer, ok = dc.ci.(driver.Queryer)
   }
   if ok {
      var nvdargs []driver.NamedValue
      var rowsi driver.Rows
      var err error
      withLock(dc, func() {
         nvdargs, err = driverArgsConnLocked(dc.ci, nil, args)
         if err != nil {
            return
         }
         rowsi, err = ctxDriverQuery(ctx, queryerCtx, queryer, query, nvdargs)
      })
      // err要么为nil,要么为ErrSkip以外的其他错误
      // ErrSkip 通常为某些可选接口不存在,可以尝试其他接口
      if err != driver.ErrSkip {
         if err != nil {
            releaseConn(err)
            return nil, err
         }
         // err != nil
         // 数据库连接的所有权转交给了rows,rows需要主动Close,以将该连接放回到free连接池中
         rows := &Rows{
            dc:          dc,
            releaseConn: releaseConn,
            rowsi:       rowsi,
         }

         // 通过context,当收到上层事件或者事务关闭的消息,rows能够自动调用Close释放连接
         rows.initContextClose(ctx, txctx)
         return rows, nil
      }
   }

   // prepare
   var si driver.Stmt
   var err error
   withLock(dc, func() {
      si, err = ctxDriverPrepare(ctx, dc.ci, query)
   })
   if err != nil {
      releaseConn(err)
      return nil, err
   }

   // execute
   ds := &driverStmt{Locker: dc, si: si}
   rowsi, err := rowsiFromStatement(ctx, dc.ci, ds, args...)
   if err != nil {
      ds.Close()
      releaseConn(err)
      return nil, err
   }

   // Note: ownership of ci passes to the *Rows, to be freed
   // with releaseConn.
   rows := &Rows{
      dc:          dc,
      releaseConn: releaseConn,
      rowsi:       rowsi,
      closeStmt:   ds,
   }

   // 同上
   rows.initContextClose(ctx, txctx)
   return rows, nil
}

sqlパッケージレベルでは、すべての接続管理アクションが実行されていることがわかります。特定の送信および受信パッケージ/パッケージプロトコルロジックは、さまざまなドライバーによって実装されています。クエリが実行されると、接続の所有権が行に転送されます。オブジェクトとは、行がClose()関数をアクティブに呼び出して、現在使用されている接続を接続プールに戻すことを意味します。

QueryRow

同様に、QueryRow()とQuery()は実際には下部にある一連のメソッドを使用し、戻り値は単なるレイヤーです。

func (db *DB) QueryRow(query string, args ...interface{}) *Row {
   return db.QueryRowContext(context.Background(), query, args...)
}

func (db *DB) QueryRowContext(ctx context.Context, query string, args ...interface{}) *Row {
   rows, err := db.QueryContext(ctx, query, args...)
   return &Row{rows: rows, err: err}
}

// Row 和 Rows 的关系
type Row struct {
   // One of these two will be non-nil:
   err  error // deferred error for easy chaining
   rows *Rows
}

注意すると、RowはScanメソッドのみを提供し、Close()も提供しないことがわかります。Rowsと比較すると、少し薄く見えます。接続を解放するにはどうすればよいですか。

RowのScan()メソッドでは、最初のデータが行から読み取られ、最後に、行のClose()メソッドが呼び出されます。

func (r *Row) Scan(dest ...interface{}) error {
   if r.err != nil {
      return r.err
   }

   defer r.rows.Close()
   for _, dp := range dest {
      if _, ok := dp.(*RawBytes); ok {
         return errors.New("sql: RawBytes isn't allowed on Row.Scan")
      }
   }

   if !r.rows.Next() {
      if err := r.rows.Err(); err != nil {
         return err
      }
      return ErrNoRows
   }
   err := r.rows.Scan(dest...)
   if err != nil {
      return err
   }
   // Make sure the query can be processed to completion with no errors.
   return r.rows.Close()
}

つまり、QueryRow()を使用する場合、結果を取得するにはrow.Scan()を使用する必要があります。そうしないと、接続が接続プールに戻されません。

Exec

Execは結果セットを必要としないため、接続の解放は最初の2つほど面倒ではなく、処理フローは基本的に同じです。

func (db *DB) execDC(ctx context.Context, dc *driverConn, release func(error), query string, args []interface{}) (res Result, err error) {
   // 调用 Exec 函数就不需要额外关心连接的release,在函数结束之前就放回free池中
   defer func() {
      release(err)
   }()
   execerCtx, ok := dc.ci.(driver.ExecerContext)
   var execer driver.Execer
   if !ok {
      execer, ok = dc.ci.(driver.Execer)
   }

   // 和Query一样,如果驱动有实现这两个接口,就直接调用,否则由sql包主动触发调用prepare+execute
   if ok {
      var nvdargs []driver.NamedValue
      var resi driver.Result
      withLock(dc, func() {
         nvdargs, err = driverArgsConnLocked(dc.ci, nil, args)
         if err != nil {
            return
         }
         resi, err = ctxDriverExec(ctx, execerCtx, execer, query, nvdargs)
      })
      if err != driver.ErrSkip {
         if err != nil {
            return nil, err
         }
         return driverResult{dc, resi}, nil
      }
   }

   var si driver.Stmt
   withLock(dc, func() {
      si, err = ctxDriverPrepare(ctx, dc.ci, query)
   })
   if err != nil {
      return nil, err
   }
   ds := &driverStmt{Locker: dc, si: si}
   defer ds.Close()

   // 从 statement 中保存结果
   return resultFromStatement(ctx, dc.ci, ds, args...)
}

8.stmtを適切に使用します

前述のように、プレースホルダーを直接使用してバイナリSQLを実行すると、実際には毎回2つのSQLが送信されるため、実行効率は向上しません。ステートメントを実行する正しい方法は何ですか。

stmt, err := db.Prepare("select * from t1 where a = ?”)   // prepare,sql_type=22
if err != nil {
   return
}
_, err = stmt.Exec(1)  // 第一次执行,sql_type=23
if err != nil {
   return
}
rows, err := stmt.Query(1)  // 第二次执行,连接所有权转交给rows,sql_type=23
if err != nil {
   return
}
_ = rows.Close()  // 归还连接的所有权

_ = stmt.Close()  // sql_type=25 

dbは接続プールオブジェクトであり、prepareを1回呼び出すだけで済み、stmtが実行されるときに、新しい接続が取得された場合、または以前に準備されていない接続がある場合は、最初にprepareを呼び出してから、executeを実行します。したがって、準備されていない接続で実行するかどうかを心配する必要はありません。
同様に、stmtがClose()を呼び出すと、すべての接続でcloseが実行され、このstmtが閉じられます。したがって、閉じる前に、このstmtが再度実行されないことを確認してください。

9.接続を解放します

前述のように、通常のクエリを実行した後、接続をfreeConn接続プールに戻す必要があります。中間接続の所有権は譲渡されますが、最終的には再利用する必要があります。実際、トランザクションを開く要求も同様です。トランザクションがコミットまたはロールバックされた後、接続が解放されます。接続解放のメソッドは、上位層から継続的に受け継がれ、接続の所有権を持つ可能性のあるすべてのオブジェクトは、メソッドへの接続の解放を受け取る可能性があります。

// 用来将使用完的连接放回到free连接池中

func (dc *driverConn) releaseConn(err error) {
   dc.db.putConn(dc, err, true)
}

func (db *DB) putConn(dc *driverConn, err error, resetSession bool) {
   // 检查连接是否还能复用
   if err != driver.ErrBadConn {
      if !dc.validateConnection(resetSession) {
         err = driver.ErrBadConn
      }
   }

   // debugGetPut 是测试信息
   db.mu.Lock()
   if !dc.inUse {
      db.mu.Unlock()
      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 err != driver.ErrBadConn && dc.expired(db.maxLifetime) {
      err = driver.ErrBadConn
   }
   if debugGetPut {
      db.lastPut[dc] = stack()
   }
   dc.inUse = false

   // 在这个连接上注册的一些statement的关闭函数
   for _, fn := range dc.onPut {
      fn()
   }
   dc.onPut = nil

   // 如果当前连接已经不可用,意味着可能会有新的连接请求,调用maybeOpenNewConnections进行检测
   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.maybeOpenNewConnections()
      db.mu.Unlock()
      dc.Close()
      return
   }

   // hook 的一个函数,用于测试,默认为nil
   if putConnHook != nil {
      putConnHook(db, dc)
   }
   added := db.putConnDBLocked(dc, nil)
   db.mu.Unlock()

   if !added {
      dc.Close()
      return
   }
}

10.接続管理

接続管理には、主に接続アプリケーション、接続の回復と再利用、および時間外接続の非同期リリースが含まれます。

接続管理の全体的なプロセスは次のとおりです

golangデータベース接続プールデータベース/ sql実現の原理の分析

11.トランザクションを開かずに接続を修正する方法

前の内容から、接続はトランザクションを開かずに要求を完了し、空きプールに戻されることがわかります。したがって、2つの選択が連続して実行されても、同じ実際のデータベースが使用されていない可能性があります。接続。保存されたプロシージャの実行を終了して出力結果を選択する場合など、一部の特別なシナリオでは、これは要件を満たしていません。

要件を単純化します。実際、接続を長時間占有したいので、トランザクションを開くことは解決策ですが、トランザクションを追加で導入すると、ロックの解放が遅れる可能性があります(例として、mysqlの2段階ロックを取り上げます)。ここでは、Contextメソッドを使用できます。達成するために、使用例

{
   var a int
   ctx := context.Background()
   cn, err := db.Conn(ctx)  // 绑定一个连接
   if err != nil {
      return
   }

   // 执行第一次查询,将连接所有权转交给rows1
   rows1, err := cn.QueryContext(ctx, "select * from t1")
   if err != nil {
      return
   }
   _ = rows1.Scan(&a)
   _ = rows1.Close() // rows1 close,将连接所有权交给cn 

   // 执行第二次查询,将连接所有权转交给rows2
   rows2, err = cn.QueryContext(ctx, "select * from t1")
   if err != nil {
      return
   }
   _ = rows2.Scan(&a)
   _ = rows2.Close() // rows1 close,将连接所有权交给cn

   // cn close,连接回收,放回free队列
   _ = cn.Close()
}

db.Conn()によって返されるsql.Connオブジェクトに関しては、driver.Connと区別する必要があります。sql.ConnはdriverConnの別のパッケージであり、ドライバーに継続的な単一データベース接続を提供します。Driver.Connは、実現する別のドライバーです。インターフェース

// Conn represents a single database connection rather than a pool of database
// connections. Prefer running queries from DB unless there is a specific
// need for a continuous single database connection.
//
// A Conn must call Close to return the connection to the database pool
// and may do so concurrently with a running query.
//
// After a call to Close, all operations on the
// connection fail with ErrConnDone.

type Conn struct {
   db *DB

   // closemu prevents the connection from closing while there
   // is an active query. It is held for read during queries
   // and exclusively during close.
   closemu sync.RWMutex

   // dc is owned until close, at which point
   // it's returned to the connection pool.
   dc *driverConn

   // done transitions from 0 to 1 exactly once, on close.
   // Once done, all operations fail with ErrConnDone.
   // Use atomic operations on value when checking value.
   done int32
}

12.接続プールのステータスを監視する

mysqlプロトコルは同期しているため、クライアントに多数の同時リクエストがあり、接続の数が同時リクエストの数より少ない場合、一部のリクエストはブロックされ、他のリクエストが接続を解放するのを待ちます。一部のシナリオまたは不適切な使用状況によっては、これもボトルネックになる可能性があります。ただし、データベースには各リクエストの接続待機時間が詳細に記録されておらず、累積待機時間やその他の監視インジケータのみが提供されており、問題を特定する際の参照として使用できます。

ライブラリはdb.Stats()メソッドを提供します。このメソッドは、dbオブジェクトからすべての監視インジケーターを取得し、オブジェクトDBStatsオブジェクトを生成します。

func (db *DB) Stats() DBStats {
   wait := atomic.LoadInt64(&db.waitDuration)

   db.mu.Lock()
   defer db.mu.Unlock()

   stats := DBStats{
      MaxOpenConnections: db.maxOpen,

      Idle:            len(db.freeConn),
      OpenConnections: db.numOpen,
      InUse:           db.numOpen - len(db.freeConn),

      WaitCount:         db.waitCount,
      WaitDuration:      time.Duration(wait),
      MaxIdleClosed:     db.maxIdleClosed,
      MaxLifetimeClosed: db.maxLifetimeClosed,
   }
   return stats
}

簡単な使用例

func monitorConn(db *sql.DB) {
   go func(db *sql.DB) {
      mt := time.NewTicker(monitorDbInterval * time.Second)
      for {
         select {
         case <-mt.C:
            stat := db.Stats()
            logutil.Errorf("monitor db conn(%p): maxopen(%d), open(%d), use(%d), idle(%d), "+
               "wait(%d), idleClose(%d), lifeClose(%d), totalWait(%v)",
               db,
               stat.MaxOpenConnections, stat.OpenConnections,
               stat.InUse, stat.Idle,
               stat.WaitCount, stat.MaxIdleClosed,
               stat.MaxLifetimeClosed, stat.WaitDuration)
         }
      }
   }(db)
}

1.15より前は、stat.MaxLifetimeClosedオブジェクトの統計が異常になり、1.15以降で修正されていることに注意してください。

注意

  • 接続所有者の転送関係に注意し、rows.Close()、row.Scan()など、使用後の時間内にリサイクルしてください。リサイクルしないと接続リークが発生し、新しいリクエストは常にブロックされます。
  • プレースホルダーを使用してSQLを実行することは避けてください。SQLスプライシングを完了するか、通常どおりstmtを使用することをお勧めします。
  • 1.15以降、単一接続のアイドル時間の制限がサポートされます
  • db.Conn()は引き続き接続を占有できますが、この接続では、前の準備によって生成されたstmtを呼び出す方法はありませんが、トランザクション内にある可能性があり、tx.Stmt()はトランザクションに固有のstmtを生成できます。
  • Goは、freeConnを目的としたデータベース接続プールのリサイクル戦略を提供します。つまり、接続が常に占有されている場合、接続が有効期間を超えても、リサイクルされません。
  • 接続プールを運用するたびに、グローバルビッグロックを先に追加する必要があることに気づきました。そのため、接続数が多く(> 1000)、リクエスト量が多いと、ロックの競合が激しくなります。大きな接続プールを複数の小さな接続プールに分割するのが簡単な方法であるため、top(sys)インジケーターとpprofから1つ見つけることができます。通常、要求は単純なポーリングによって送信されます。複数の接続プールに散在しているため、ロックの粒度を効果的に減らすことができます

【終了】

おすすめ

転載: blog.51cto.com/muhuizz/2577451