Principio e implementación de la tecnología del grupo de conexiones de la base de datos de Golang

1 ¿Por qué necesita un grupo de conexiones?

Si no usa un grupo de conexiones, es más costoso crear una conexión para cada solicitud, por lo que debe completar 3 protocolos de enlace tcp. Al mismo tiempo, en escenarios de alta concurrencia, dado que no hay límite en el número máximo de conexiones en el grupo de conexiones, se pueden crear innumerables conexiones y se agotarán los descriptores de archivos. El grupo de conexiones es para reutilizar algunas conexiones creadas.

2 Diseño de piscina de conexión

Básicamente, el grupo de conexiones diseñará los siguientes parámetros:

Número inicial de conexiones : el número de conexiones que se crearán previamente cuando se inicialice el conjunto de conexiones, si se establece:

  • Demasiado grande: potencialmente derrochador

  • Demasiado pequeño: se debe crear una nueva conexión cuando llega la solicitud

El número máximo de conexiones inactivas maxIdle : el número máximo de conexiones en caché en el grupo, si se establece:

  • Demasiado grande: causa desperdicio, aún necesita controlar la conexión. Debido a que la cantidad total de conexiones a la base de datos es limitada, si el proceso actual ocupa demasiado, otros procesos pueden obtener menos.

  • Demasiado pequeño: incapaz de hacer frente al tráfico de ráfagas

El número máximo de conexiones maxCap :

  • Si ya ha utilizado conexiones maxCap, cuando desee solicitar la conexión maxCap+1th, generalmente se bloqueará allí hasta que se agote el tiempo o alguien más devuelva una conexión

El tiempo máximo de inactividad idleTimeout : cuando se encuentra que una conexión está inactiva durante más de este tiempo, se cerrará y se volverá a obtener la conexión

  • Evita el problema de que la conexión es inservible durante mucho tiempo y caduca automáticamente

El grupo de conexiones proporciona dos métodos para el mundo exterior, Obtener: Obtener una conexión, Poner: Devolver una conexión. La implementación de la mayoría de los grupos de conexiones es similar y el proceso básico es el siguiente:

imagen

3 Grupo de conexiones SQL de la biblioteca estándar de Golang

El grupo de conexiones de Golang se implementa bajo la base de datos de la biblioteca estándar/sql/sql.go. Cuando corremos:

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

Se abrirá un grupo de conexiones. Puede mirar la estructura de la base de datos devuelta:

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

Se puede ver que freeConn, la estructura de conexión de almacenamiento interno del conjunto de conexiones de la base de datos, no es el canal que se usaba antes, sino []*driverConn, un segmento de conexión.


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

Continúe mirando el código, mire hacia atrás a través del método de consulta, podemos ver esta función:

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

3.1  Obtener conexión

// 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  conexión de liberación

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
}

Por lo tanto, se deben realizar los siguientes puntos durante el proceso de desarrollo:

Reutilización de recursos: las conexiones de la base de datos se reutilizan, lo que evita una gran cantidad de sobrecarga de rendimiento causada por la creación y liberación frecuente de conexiones.

Velocidad de respuesta del sistema más rápida: durante el proceso de inicialización del grupo de conexiones de la base de datos, a menudo se crean varias conexiones de bases de datos y se colocan en el grupo en espera. Para el procesamiento de solicitudes comerciales, las conexiones disponibles existentes se utilizan directamente para evitar la sobrecarga de tiempo del proceso de inicialización y liberación de la conexión de la base de datos, lo que reduce el tiempo de respuesta general del sistema.

Nuevos medios de asignación de recursos: para un sistema en el que varias aplicaciones comparten la misma base de datos, la tecnología del grupo de conexiones de la base de datos se puede realizar mediante la configuración de la conexión de la base de datos en la capa de la aplicación. Establezca un límite en la cantidad máxima de conexiones de base de datos disponibles para una aplicación para evitar que una aplicación monopolice todos los recursos de la base de datos.

Gestión unificada de conexiones para evitar fugas en la conexión de la base de datos: en una implementación de grupo de conexiones de base de datos relativamente completa, la conexión ocupada se puede recuperar a la fuerza de acuerdo con la configuración de tiempo de espera de ocupación de conexión preestablecida. Esto evita pérdidas de recursos que pueden ocurrir durante las operaciones regulares de conexión a la base de datos.

Supongo que te gusta

Origin blog.csdn.net/ygq13572549874/article/details/131819924
Recomendado
Clasificación