Análisis del principio de la base de datos del grupo de conexiones de la base de datos golang / realización de sql

La solicitud de Golang a la base de datos abstrae un conjunto de grupos de conexiones universales. Con el mecanismo go, golang solo necesita proporcionar una interfaz de controlador, y los diferentes protocolos de base de datos subyacentes son controlados por los usuarios de acuerdo con su propia base de datos. pueden.

Este artículo explora los detalles aquí y los pozos que deben evitarse desde la perspectiva de la implementación del código fuente. Según el análisis del código 1.14, algunos errores se han corregido u optimizado en 1.15, que también se mencionarán aquí.

versión de golang: 1.14

Descripción de la estructura del directorio

└── 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

Estructura de datos principal

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 no es una conexión. Es una interfaz abstracta de la base de datos y un identificador para todo el grupo de conexiones. Es seguro para múltiples rutinas gordas. Puede abrir y cerrar conexiones de base de datos según el controlador y administrar el grupo de conexiones. Esto es igual para diferentes bases de datos.

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
}

Encapsulación de una sola conexión, incluida la conexión real de la base de datos y la información de estado relacionada, etc.

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

Una conexión de base de datos específica debe ser implementada por diferentes controladores.

4. conductor Conductor

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

El controlador contiene solo una función, Open () se usa para devolver una conexión disponible, que puede ser una conexión recién establecida o una conexión cerrada que se almacenó en caché anteriormente.

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

El propósito de DriverContext es mantener la información del contexto del controlador, evitando la necesidad de analizar dsn cada vez que se crea una nueva conexión. El objeto Driver debe implementarse por sí mismo.

6. conductor.Conector

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 es el socket del driver, un objeto de tipo interfaz, que es implementado por diferentes tipos de bases de datos.
driver.Connector contiene dos funciones.

  • Connect se utiliza para establecer una conexión
  • El controlador se utiliza para devolver un objeto de controlador. El controlador también es un objeto de tipo interfaz y debe ser implementado por diferentes bases de datos.

Proceso de operación principal

1. Registre el conductor

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 proporciona un grupo de conexiones de base de datos general.Cuando nos conectamos a diferentes bases de datos, solo necesitamos registrar el controlador de base de datos correspondiente para usarlo.

El registro aquí es en realidad para agregar el nombre de la base de datos y el controlador de la base de datos correspondiente (envoltorio de conexión de la base de datos) a un mapa.Cada biblioteca importada debe implementarse llamando a la función de registro en la función init.

2. Cree un controlador de grupo de conexiones 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
}

La función Open se suele interpretar como inicializar la base de datos. Aquí, el controlador correspondiente se obtiene a través del nombre del controlador, y se realizan una serie de operaciones de inicialización en el controlador. Cabe señalar que Open no establece una conexión con la base de datos, sino que solo opera estas estructuras de datos. Para iniciar acciones como corrutinas en segundo plano.

El nombre de origen de datos aquí se conoce como dsn, que contiene los parámetros necesarios para conectarse a la base de datos, nombre de usuario, contraseña, puerto IP y otra información. Los diferentes controladores implementan el análisis por sí mismos. Por supuesto, algunos controladores también admiten la configuración de algunos parámetros de la base de datos en dsn, como autocommit. Debido a que la información obtenida al analizar la cadena consumirá una cierta cantidad de recursos, también proporciona la función de almacenar en caché el resultado analizado, evitando la necesidad de analizarlo cada vez que se establece una nueva conexión. Para hacer esto, necesita ser manejado. driver.DriverContext interfaz.

En este momento tiene una estructura de este tipo, pero no hay conexión en el grupo de conexiones en este momento, lo que significa que no hay acceso real a db

Análisis del principio de la base de datos del grupo de conexiones de la base de datos golang / realización de sql

3. Establecer parámetros de conexión a la base de datos

El número máximo de conexiones inactivas. Si el número de conexiones inactivas supera este valor, se cerrarán. El valor predeterminado es defaultMaxIdleConns (2)

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

El número máximo de conexiones abiertas permitidas. Una vez superado este número, no se permite establecer nuevas conexiones y la rutina de trabajo solo puede bloquearse esperando la liberación de conexiones

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

El tiempo máximo que se puede reutilizar una conexión, en otras palabras, cuánto tiempo se cerrará una conexión después de que se cierre, pero se cerrará después de que se complete la solicitud actual, se cerrará perezosamente, un parámetro de muy mal gusto

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

Después de 1.15, agregue parámetros, el tiempo máximo de inactividad de la conexión, el tiempo de inactividad se cerrará si el tiempo de inactividad excede este valor, pero se cerrará después de que se complete la solicitud actual, se cerrará perezosamente

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

Implementación detallada de SetConnMaxLifetime y SetConnMaxIdleTime

  • 1.14 Implementación
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 Implementación
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
}

La lógica de implementación de 1.14 y 1.15 es básicamente la misma, pero agrega compatibilidad con el juicio del tiempo de espera inactivo

4. Accede a la base de datos

Después de haber realizado las acciones de inicialización anteriores, de acuerdo con nuestros hábitos, generalmente intentamos conectarnos a la base de datos para determinar si los parámetros de conexión son normales, como si el nombre de usuario y la contraseña son correctos, pero sin enviar solicitudes de usuario. El enfoque general es llamar 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. Enviar solicitud sql

Estas son algunas de las formas más sencillas de enviar 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 {}

Aquí hay algunas notas:

  • Podemos encontrar que cada método tiene otro método con el sufijo Context al mismo tiempo. Si observa la relación de llamada, encontrará que la función sin Context (Exec / Query / QueryRow) es en realidad la función con Context (ExecContext / QueryContext / QueryRowContext), el contexto aquí es el mismo que la mayoría de las funciones de la biblioteca, que se utilizan para la sincronización de señales, como los límites de tiempo de espera, etc., generalmente no es necesario configurarlo por separado
  • Podemos encontrar que cada parámetro de función admite una lista de parámetros variables, y el uso es el mismo que el de prepare.Use? Como marcador de posición, luego escribimos directamente SQL o usamos un marcador de posición, ¿cuál es mejor?
    rows1, err := db.Query("select * from t1 where a = 1”)
    rows2, err := db.Query("select * from t1 where a = ?", 1)

Los resultados de estas dos ejecuciones de SQL son los mismos, pero la capa inferior es diferente, que es ligeramente diferente de la implementación específica de diferentes controladores.

Tome mysql como ejemplo, la diferencia es que la primera Consulta envió un sql (sql_type: 3) y la segunda Consulta envió dos sql (sql_type: 22 y sql_tyep: 23), primero se prepara y luego se ejecuta, Aunque el protocolo binario es más rápido, enviará dos sql cada vez. La preparación enviada por primera vez solo se ejecutará una vez después de eso y la información de preparación no se reciclará activamente.

Al comienzo del diseño de esta interfaz, debe diseñarse de acuerdo con la idea de preparar + ejecutar. Cuando el número de parámetros de marcador de posición es 0, si puede optimizar y enviar un sql directamente depende de si la interfaz del controlador subyacente lo admite, en otras palabras, preparar + ejecutar

A continuación, tome Query como ejemplo para ver el proceso de implementación específico

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

Se puede encontrar que se requieren dos pasos para ejecutar un SQL ordinario: el primer paso es obtener la conexión (db.conn) y el segundo paso es ejecutar la consulta (db.queryDC).

6. Consiga la conexión

// Proporciona dos estrategias para obtener conexiones, alwaysNewConn y cachedOrNewConn, literalmente, siempre crea nuevas y prioriza la reutilización de conexiones gratuitas

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
}

En resumen, cuando solicitamos una conexión al grupo de conexiones,

  • Si la estrategia está en cachéOrNewConn y está en el grupo de conexiones libres, se eliminará directamente;
  • Si no hay una conexión inactiva en el grupo de conexiones o la política es alwaysNewConn, y la conexión actual no supera el límite superior, se creará directamente;
  • De lo contrario, el canal se utiliza para crear y establecer de forma asincrónica, y el sitio de la llamada bloquea y espera la conexión.

7. Ejecutar consulta

Consulta

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

Se puede encontrar que en el nivel del paquete sql, se han realizado todas las acciones de administración de la conexión. La lógica específica del protocolo de envío y recepción del paquete / paquete es implementada por diferentes controladores. Cuando se ejecuta la consulta, la propiedad de la conexión se transfiere a las filas Objeto significa que las filas llaman activamente a la función Close () para volver a poner la conexión utilizada actualmente en el grupo de conexiones.

QueryRow

De manera similar, QueryRow () y Query () en realidad usan un conjunto de métodos en la parte inferior, y el valor de retorno es solo una capa

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
}

Si tiene cuidado, puede encontrar que Row solo proporciona un método de escaneo, e incluso Close () no lo es. Comparado con Rows, parece un poco delgado, entonces, ¿cómo liberar la conexión?

En el método Scan () de Row, se lee el primer dato de las filas y, al final, se llama al método Close () de filas

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

Significa que cuando usamos QueryRow (), debemos usar row.Scan () para obtener el resultado; de lo contrario, la conexión no se volverá a colocar en el grupo de conexiones.

Ejecutivo

Exec no requiere un conjunto de resultados, por lo que la liberación de la conexión no es tan problemática como las dos primeras y el flujo de procesamiento es básicamente el mismo.

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. Utilice stmt con elegancia

Como se mencionó anteriormente, el uso directo de marcadores de posición para ejecutar sql binario en realidad enviará dos sql cada vez, lo que no mejora la eficiencia de ejecución ¿Cuál es la forma correcta de ejecutar la instrucción?

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 

Sabemos que db es un objeto de grupo de conexiones, donde prepare solo necesita ser llamado una vez, y luego, cuando se ejecuta stmt, si se obtiene una nueva conexión o una conexión que no se ha preparado antes, primero llamará a prepare y luego ejecutará execute Por lo tanto, no debemos preocuparnos de si ejecutaremos en una conexión que no se ha preparado.
De manera similar, cuando stmt llama a Close (), se ejecutará close en todas las conexiones y cerrará este stmt, por lo tanto, antes de cerrar, asegúrese de que este stmt no se vuelva a ejecutar.

9. Libere la conexión

Como se mencionó anteriormente, nuestra conexión debe devolverse al grupo de conexiones freeConn a tiempo después de ejecutar una consulta ordinaria. Aunque la propiedad de la conexión intermedia se transferirá, eventualmente deberá reciclarse. De hecho, la solicitud para abrir una transacción es similar. La conexión se libera después de que la transacción se confirma o se revierte. El método de liberación de la conexión se transmite continuamente desde la capa superior, y todos los objetos que pueden tener la propiedad de la conexión pueden recibir la liberación de la conexión al método.

// 用来将使用完的连接放回到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. Gestión de conexiones

La gestión de la conexión incluye principalmente la aplicación de la conexión, la recuperación y reutilización de la conexión y la liberación asincrónica de las conexiones en horas extraordinarias.

Todo el proceso de gestión de conexiones es el siguiente

Análisis del principio de la base de datos del grupo de conexiones de la base de datos golang / realización de sql

11. Cómo arreglar una conexión sin abrir la transacción

A través del contenido anterior, se puede encontrar que la conexión completa una solicitud sin abrir la transacción y se devuelve al grupo libre, por lo que incluso si se ejecutan dos selecciones de forma consecutiva, es posible que no se utilice la misma base de datos real Conexión, para algunos escenarios especiales, como cuando terminamos de ejecutar un procedimiento almacenado y queremos seleccionar un resultado de salida, esto no cumple con los requisitos.

Simplifique los requisitos, de hecho, queremos ocupar una conexión durante mucho tiempo, abrir una transacción es una solución, pero la introducción adicional de transacciones puede provocar la liberación retrasada del bloqueo (tome el bloqueo de dos etapas de mysql como ejemplo), aquí puede usar el método Context Para lograr, ejemplos de uso

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

Con respecto al objeto sql.Conn devuelto por db.Conn (), debe distinguirse de driver.Conn. Sql.Conn es otro paquete de driverConn, que proporciona una conexión de base de datos única continua para el controlador. Driver.Conn es un controlador diferente para lograr Interfaz

// 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. Supervisar el estado del grupo de conexiones

Debido a que el protocolo mysql es síncrono, cuando el cliente tiene una gran cantidad de solicitudes simultáneas, pero la cantidad de conexiones es menor que la cantidad de solicitudes simultáneas, algunas de las solicitudes se bloquearán y esperarán a que otras solicitudes liberen la conexión. En algunos escenarios o uso inadecuado Dadas las circunstancias, esto también puede convertirse en un cuello de botella. Sin embargo, la base de datos no registra el tiempo de espera de conexión de cada solicitud en detalle, solo proporciona el tiempo de espera acumulado y otros indicadores de monitoreo, que pueden usarse como referencia para localizar problemas.

La biblioteca proporciona el método db.Stats (), que obtendrá todos los indicadores de seguimiento del objeto db y generará el objeto 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
}

Un ejemplo de uso simple

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

Cabe señalar que antes de 1.15, las estadísticas del objeto stat.MaxLifetimeClosed serán anormales, y se ha corregido después de 1.15.

Atención

  • Preste atención a la relación de transferencia del propietario de la conexión y recíclela a tiempo después de su uso, como las filas.Cerrar (), fila.Scan (), etc. Si no se recicla, se producirán fugas en la conexión y las solicitudes nuevas siempre se bloquearán
  • Trate de evitar el uso de marcadores de posición para ejecutar SQL. Se recomienda completar el empalme de SQL o usar stmt normalmente.
  • Después de la 1.15, se admite la restricción del tiempo de inactividad de una sola conexión
  • db.Conn () puede continuar ocupando una conexión, pero en esta conexión, no hay forma de llamar al stmt generado por la preparación anterior, pero puede estar en la transacción, tx.Stmt () puede generar stmt específico para la transacción
  • Go proporciona una estrategia de reciclaje del grupo de conexiones de la base de datos, que está dirigida a freeConn. En otras palabras, si la conexión está ocupada todo el tiempo, incluso si ha excedido la vida útil, no se reciclará.
  • Notamos que cada vez que se opera un grupo de conexiones, primero se debe agregar un candado grande global. Por lo tanto, cuando el número de conexiones es grande (> 1000) y el volumen de solicitudes es grande, habrá una competencia de candados más seria. Una cosa se puede encontrar a través del indicador top (sys) y pprof, porque una forma simple es dividir un grupo de conexiones grande en varios grupos de conexiones pequeños.En general, la solicitud se envía mediante un sondeo simple. Dispersos en múltiples grupos de conexiones, pueden reducir efectivamente la granularidad del bloqueo

【Terminar】

Supongo que te gusta

Origin blog.51cto.com/muhuizz/2577451
Recomendado
Clasificación