Golang データベース接続プール技術の原理と実装

1 なぜ接続プールが必要なのでしょうか?

接続プールを使用しない場合、リクエストごとに接続を作成するとコストが高くなるため、3 つの TCP ハンドシェイクを完了する必要があります。同時に、同時実行性の高いシナリオでは、接続プール内の接続の最大数に制限がないため、無数の接続が作成され、ファイル記述子が枯渇する可能性があります。接続プールは、作成された一部の接続を再利用するためのものです。

2 接続プールの設計

基本的に、接続プールは次のパラメータを設計します。

初期接続数: 接続プールの初期化時に事前に作成される接続の数 (設定されている場合):

  • 大きすぎる: 無駄になる可能性がある

  • 小さすぎる: リクエストが来たときに新しい接続を作成する必要がある

アイドル接続の最大数 maxIdle : プール内のキャッシュされた接続の最大数 (設定されている場合):

  • 大きすぎる: 無駄が発生するため、接続を制御する必要があります。データベースへの全体的な接続数は制限されているため、現在のプロセスが多くを占めると、他のプロセスの接続数が少なくなる可能性があります。

  • 小さすぎる: バースト トラフィックに対処できない

最大接続数 maxCap :

  • すでに maxCap 接続を使用している場合、maxCap+1 番目の接続を申請する場合、通常はタイムアウトになるか他の誰かが接続を返すまでそこでブロックされます。

最大アイドル時間 idleTimeout : 接続がこの時間を超えてアイドル状態であることが判明した場合、その接続は閉じられ、接続が再度取得されます。

  • 接続が長時間役に立たず、自動的に期限切れになる問題を回避します

接続プールは、Get: 接続を取得する、Put: 接続を返すという 2 つのメソッドを外部に提供します。ほとんどの接続プールの実装は類似しており、基本的なプロセスは次のとおりです。

写真

3 Golang標準ライブラリSQL接続プール

Golang の接続プールは、標準ライブラリの database/sql/sql.go の下に実装されています。実行すると:

db, err := sql.Open("mysql", "xxxx")

接続プールが開かれます。返されたデータベースの構造を確認できます。

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 // 锁
    freeConn     []*driverConn // 可用连接池
    connRequests map[uint64]chan connRequest // 连接请求表,key 是分配的自增键
    nextRequest  uint64 // 连接请求的自增键
    numOpen      int    // 已经打开 + 即将打开的连接数
    // 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{} // 告知 connectionOpener 需要新的连接
    resetterCh        chan *driverConn // connectionResetter 函数,连接放回连接池的时候会用到
    closed            bool
    dep               map[finalCloser]depSet
    lastPut           map[*driverConn]string // debug 时使用,记录上一个放回的连接
    maxIdle           int                    // 连接池大小,默认大小为 2,<= 0 时不使用连接池
    maxOpen           int                    // 最大打开的连接数,<= 0 不限制
    maxLifetime       time.Duration          // 一个连接可以被重用的最大时限,也就是它在连接池中的最大存活时间,0 表示可以一直重用
    cleanerCh         chan struct{} // 告知 connectionCleaner 清理连接
    waitCount         int64 // 等待的连接总数
    maxIdleClosed     int64 // 释放连接时,因为连接池已满而被关闭的连接总数
    maxLifetimeClosed int64 // 因为超过存活时间而被关闭的连接总数

    stop func() // stop cancels the connection opener and the session resetter.
}
 
 

DB 接続プールの内部ストレージ接続構造である freeConn は、以前に使用されていた chan ではなく、接続スライスである []*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  // 锁
    ci          driver.Conn // 对应具体的连接
    closed      bool // 是否标记关闭
    finalClosed bool // 是否最终关闭
    openStmt    map[*driverStmt]bool // 在这个连接上打开的状态
    lastErr     error // connectionResetter 的返回结果

    // guarded by db.mu
    inUse      bool // 连接是否占用
    onPut      []func() // 连接归还时要运行的函数,在 noteUnusedDriverStatement 添加
    dbmuClosed bool     // 和 closed 状态一致,但是由锁保护,用于 removeClosedStmtLocked
}
 
 

引き続きコードを確認し、クエリ メソッドを最後まで遡って確認すると、次の関数が表示されます。

func(db*DB)conn(ctx context.Context,strategy connReuseStrategy)(*driverConn,error)。

3.1 接続の取得

// conn returns a newly-opened or cached *driverConn.
func (db *DB) conn(ctx context.Context, strategy connReuseStrategy) (*driverConn, error) {
    // 先判断db是否已经关闭。
  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
    // 这边如果在freeConn这个切片有空闲连接的话,就left pop一个出列。注意的是,这边因为是切片操作,所以需要前面需要加锁且获取后进行解锁操作。同时判断返回的连接是否已经过期。
    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()
        if conn.expired(lifetime) {
            conn.Close()
            return nil, driver.ErrBadConn
        }
        // Lock around reading lastErr to ensure the session resetter finished.
        conn.Lock()
        err := conn.lastErr
        conn.Unlock()
        if err == driver.ErrBadConn {
            conn.Close()
            return nil, driver.ErrBadConn
        }
        return conn, nil
    }
    // 这边就是等候获取连接的重点了。当空闲的连接为空的时候,这边将会新建一个request(的等待连接 的请求)并且开始等待
    if db.maxOpen > 0 && db.numOpen >= db.maxOpen {
        // 下面的动作相当于往connRequests这个map插入自己的号码牌。
        // 插入号码牌之后这边就不需要阻塞等待继续往下走逻辑。
        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():
            // context取消操作的时候,记得从connRequests这个map取走自己的号码牌。
            db.mu.Lock()
            delete(db.connRequests, reqKey)
            db.mu.Unlock()
            atomic.AddInt64(&db.waitDuration, int64(time.Since(waitStart)))
            select {
            default:
            case ret, ok := <-req:
                // 这边值得注意了,因为现在已经被context取消了。但是刚刚放了自己的号码牌进去排队里面。意思是说不定已经发了连接了,所以得注意归还!
                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
            }
            if ret.err == nil && ret.conn.expired(lifetime) {
                ret.conn.Close()
                return nil, driver.ErrBadConn
            }
            if ret.conn == nil {
                return nil, ret.err
            }
            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
        }
    }
    // 下面就是如果上面说的限制情况不存在,可以创建先连接时候,要做的创建连接操作了。
    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
}
 
 

3.2 リリース接続

func (db *DB) putConnDBLocked(dc *driverConn, err error) bool {
  if db.closed {
    return false
  }
  if db.maxOpen > 0 && db.numOpen > db.maxOpen {
    return false
  }
  // 这边是重点了,基本来说就是从connRequest这个map里面随机抽一个
  // 在排队等着的请求。取出来后发给他。就不用归还池子了。
  if c := len(db.connRequests); c > 0 {
    var req chan connRequest
    var reqKey uint64
    for reqKey, req = range db.connRequests {
      break
    }
    delete(db.connRequests, reqKey) // Remove from pending requests.
    if err == nil {
      dc.inUse = true
    }
    // 把连接给这个正在排队的连接
    req <- connRequest{
      conn: dc,
      err:  err,
    }
    return true
  } else if err == nil && !db.closed {
   // 既然没人排队,就看看到了最大连接数目没有。没到就归还给freeConn
    if db.maxIdleConnsLocked() > len(db.freeConn) {
      db.freeConn = append(db.freeConn, dc)
      db.startCleanerLocked()
      return true
    }
    db.maxIdleClosed++
  }
  return false
}

したがって、開発プロセスでは次の点を行う必要があります。

リソースの再利用:データベース接続が再利用され、接続の頻繁な作成と解放によって引き起こされる大量のパフォーマンスのオーバーヘッドが回避されます。

システムの応答速度の高速化:データベース接続プールの初期化プロセス中に、多くの場合、複数のデータベース接続が作成され、スタンバイ用にプールに配置されます。ビジネス リクエストの処理では、データベース接続の初期化および解放プロセスにかかる時間のオーバーヘッドを回避するために、既存の利用可能な接続が直接使用され、それによってシステム全体の応答時間が短縮されます。

リソース割り当ての新しい手段:複数のアプリケーションが同じデータベースを共有するシステムでは、アプリケーション層でのデータベース接続の構成を通じてデータベース接続プール テクノロジを実現できます。アプリケーションがすべてのデータベース リソースを独占しないように、アプリケーションで使用できるデータベース接続の最大数に制限を設定します。

データベース接続リークを回避するための統合接続管理:比較的完全なデータベース接続プールの実装では、事前に設定された接続占有タイムアウト設定に従って、占有されている接続を強制的に回復できます。これにより、通常のデータベース接続操作中に発生する可能性のあるリソース リークが回避されます。

おすすめ

転載: blog.csdn.net/ygq13572549874/article/details/131819924