go database/sql 库 连接池管理

本文详细探讨了Go的database/sql库中连接池的管理,包括DB的连接数变量、连接建立与释放时机。重点解析了Open、Query过程中的源码,阐述了连接池如何根据maxIdle和maxOpen策略管理连接,以及连接的建立、释放和重用机制。

1.DB的有关连接数变量

type DB struct {
.....
freeConn       []*driverConn   //空闲连接队列
connRequests   map[uint64]chan connRequest  //正在阻塞的sql请求,等有空闲连接来处理
numOpen        int  //程序内已开启和正在开启的连接数
maxIdle        int  //连接池内空闲连接的最大个数
maxOpen        int  //连接池内最大可以开启的连接总数
.....
}

一定要弄明白以上几个连接数的含义:

maxIdlemaxOpen是两个比较重要的阈值,分别可以通过SetMaxIdleConnsSetMaxOpenConns函数设置,如果maxIdle>maxOpen, 则maxIdle=maxOpen,所以是maxOpen起决定作用

maxOpen<=0表示没有限制,不建议这样,否则缓存穿透/缓存击穿/缓存雪崩时大量连接过来就会压垮数据库。

 

freeConn 是真正可以被请求拿去使用的连接实体,它的长度就是程序实时空闲连接数;

numOpen  是程序实时已开启或正在开启的连接数;

当前程序正在被使用的连接数= numOpen - freeConn

 

当len(freeConn) < maxIdle时, 被请求释放的已使用完的连接就可以放入freeConn队列中,否则这个连接就要被关闭。

当发现connRequests长度不为0时,说明有请求被阻塞,这时如果numOpen<maxOpen,那么就可以继续开启新的连接,连接数为min(maxOpen-numOpen,len(connRequests))。

 

2.连接的建立与释放时机

func Open(driverName, dataSourceName string) (*DB, error)

调用open时并没有创建连接,这个时候返回的DB只是数据库的句柄,用于记录数据库的具体信息。它只是把dataSourceName这个url按照标准格式解析了。

 

那什么时候才建立第一次连接呢?

1. 调用DB.Ping()接口

2.调用DB.Exec()/DB.Query()等接口

3.调用DB.Begin()接口新建事务时,也是这个时候就把连接拿到。所以拿到事务如果不立刻处理,就会很浪费连接,因为这个时候的连接将一直被占用。

注意,上面说的是第一次,因为连接池有空闲且正常连接时,那就可以不用建立连接,直接取这些连接了。

 

连接的释放:

DB.Ping()接口/DB.Exec()/DB.Query()等接口,都是在接口调用完成之前释放连接,DB.Begin()接口的连接要等事务关闭时才能释放连接。释放的连接根据前面说的由连接池内空闲连接数决定回收还是关闭。

 

3.以一次query请求为例解读源码

database/sql其实就是符合桥接模式,把各个数据库要实现的公共的方法抽取出来比如连接池管理,再定义一套接口,由具体每种数据库实体实现。

3.1调用Open获取DB时

注意到Open函数内调用了OpenDB,而OpenDB又起了两个协程,如下:

func OpenDB(c driver.Connector) *DB {
.....
    go db.connectionOpener(ctx)
    go db.connectionResetter(ctx)
.....
}

3.1.1 connectionOpener函数

主要就是等待接受db.openerCh这个chan内的信号进行新建连接,如下:

func (db *DB) connectionOpener(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            return
        case <-db.openerCh:
            db.openNewConnection(ctx)
        }
    }
}

openNewConnection函数就是通过调用底层具体数据库接口起一个连接。

      如果这个连接成功建立了就调用db.putConnDBLocked(dc, err)函数决定这个新起的连接是放入空闲队列里还是直接给阻塞的请求去使用,还是超出了maxOpen需要被关闭。

      如果底层接口调用失败了呢,那就再重试啊,所以调用db.maybeOpenNewConnections(),根据阻塞请求数和还可以起的连接数去决定下次需要新建的连接数。

 

3.1.2 connectionResetter函数

主要就是等待接受db.resetterCh这个chan内的信号进行清除连接的一些信息,具体就是调用底层数据库提供的ResetSession接口,如下:

func (db *DB) connectionResetter(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            close(db.resetterCh)
            for dc := range db.resetterCh {
                dc.Unlock()
            }
            return
        case dc := <-db.resetterCh:
            dc.resetSession(ctx)
        }
    }
}

 

 

3.2 调用Query时

Query-> QueryContext-> query,前面调用链不重要,下面看query接口的代码:

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

是的,由上面可以清晰的看出分两步,第一步先去拿连接,第二步在拿到的连接上进行查询并释放连接。

3.2.1 db.conn函数

它主要就是拿连接,其实可以分为三种情况:

第一种,连接池还有空闲连接,这个时候就要把连接从空闲连接移出,并校验一下连接是否正常,如下:

    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
    }
第二种,连接数已经达到了db.maxOpen上限了,这个时候又不能再新建连接了,那就只能阻塞着等忙碌的连接释放。主要看case ret, ok := <-req。但是这个时候select居然没有超时返回错误!那用户就会因为这个请求一直被挂起!!!!!
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.
		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():
			// 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)))

			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
			}
			if ret.err == nil && ret.conn.expired(lifetime) {
				ret.conn.Close()
				return nil, driver.ErrBadConn
			}
			if ret.conn == nil {
				return nil, ret.err
			}
			// Lock around reading lastErr to ensure the session resetter finished.
			ret.conn.Lock()
			err := ret.conn.lastErr
			ret.conn.Unlock()
			if err == driver.ErrBadConn {
				ret.conn.Close()
				return nil, driver.ErrBadConn
			}
			return ret.conn, ret.err
		}
	}

第三种,有没有超出最大连接数,又没有空闲连接,那就只能去新建连接了,连接是否能成功建立,重试的逻辑与前面connectionOpener类似,如下:

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.2 db.queryDC函数

queryDC就是真正在连接上处理请求了,并释放连接。是的看它的请求参数dc.releaseConn,这个函数就是释放连接。追踪一下:

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

在putConn函数里看到这里才是真正干活的:

    added := db.putConnDBLocked(dc, nil)
    db.mu.Unlock()

    if !added {
        if resetSession {
            dc.Unlock()
        }
        dc.Close()
        return
    }
    if !resetSession {
        return
    }
    select {
    default:
        // If the resetterCh is blocking then mark the connection
        // as bad and continue on.
        dc.lastErr = driver.ErrBadConn
        dc.Unlock()
    case db.resetterCh <- dc:
    }

是的,主要取决于putConnDBLocked函数,前面 3.1.1 connectionOpener函数 说过了这个函数是 决定这个新起的连接是放入空闲队列里还是直接给阻塞的请求去使用,还是超出了maxOpen需要被关闭。

再看它最后发送给db.resetterCh这个chan就是open时就起的协程处理连接重置。

 
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值