go库源码学习分析(一)-数据库连接池

1,572 阅读5分钟

每次进行查询语句之前,如果都对数据库进行连接操作,将是消耗了无用的时间,如果使用连接池的话,将会减少连接的耗时,下面是对使用连接池和不使用连接池的测试

//不使用连接池
func BenchmarkHello2(b *testing.B) {
	open, _ := sql.Open("mysql", "root:root@(127.0.0.1:3306)/go_test?charset=utf8&"+
		"parseTime=True&loc=Local")
	open.SetMaxOpenConns(100)
	open.SetMaxIdleConns(0)
	b.ResetTimer()
	for i := 0; i <= b.N; i++ {
		open.Exec("select * from user ")
	}
}
###使用连接池
func BenchmarkHello3(b *testing.B) {
	open, _ := sql.Open("mysql", "root:root@(127.0.0.1:3306)/go_test?charset=utf8&"+
		"parseTime=True&loc=Local")
	open.SetMaxOpenConns(100)
	open.SetMaxIdleConns(8)
	b.ResetTimer()
	for i := 0; i <= b.N; i++ {
		open.Exec("select * from user ")
	}
}




上面的结果,我的系统使用连接池将是不使用连接池的十倍速度之差

下面通过分析go是如何实现数据库的连接池,了解连接池的内部实现,也学习下go代码的设计

  1. 连接池常用的方法
db,err := sql.Open("mysql","root") //链接操作
db.SetMaxOpenConns(100) //设置最大连接数
db.SetMaxIdleConns(8) //设置连接池  不设置默认为2 设置为0不使用
db.SetConnMaxLifetime(1000) //一个链接的最大超时时间
db.Exec() //执行一个查询
  1. 创建连接池
//连接池的创建
func Open(driverName, dataSourceName string) (*DB, error) {
	//使用锁,获取当前使用的驱动
	//对于解锁的操作,并没有使用defer操作,对于肉眼可见的操作,建议使用完了就直接解锁,而不要使用defer
	driversMu.RLock()
	driveri, ok := drivers[driverName]
	driversMu.RUnlock()
	if !ok {
		return nil, fmt.Errorf("sql: unknown driver %q (forgotten import?)", driverName)
	}
    //判断该驱动是否实现了(driver.DriverContext)类型
	if driverCtx, ok := driveri.(driver.DriverContext); ok {
	    //调用驱动实现的链接方法
		connector, err := driverCtx.OpenConnector(dataSourceName)
		if err != nil {
			return nil, err
		}
		//创建数据库连接池
		return OpenDB(connector), nil
	}

	return OpenDB(dsnConnector{dsn: dataSourceName, driver: driveri}), nil
}
### 连接池对象的生成
func OpenDB(c driver.Connector) *DB {
    //构建上下文
	ctx, cancel := context.WithCancel(context.Background())
	//生成连接池对象
	db := &DB{
		connector:    c,
		openerCh:     make(chan struct{}, connectionRequestQueueSize),
		resetterCh:   make(chan *driverConn, 50),
		lastPut:      make(map[*driverConn]string),
		connRequests: make(map[uint64]chan connRequest),
		stop:         cancel,
	}
    //启动两个go函数,这两个函数稍后看
	go db.connectionOpener(ctx)
	go db.connectionResetter(ctx)
    //返回对象
	return db
}

连接池的创建代码,可以知道下面几个点

  1. 需要自己实现驱动,go也为我们使用了 _ "github.com/go-sql-driver/mysql" 在连接池的创建时候需要引入这个驱动实现
  2. 在创建链接的时候,go并没有真正的去链接数据库,只是返回了一个连接池对象
  3. 启动了两个go协程去处理

连接池的对象返回了,真正创建连接池的时候,是在执行exec,query方法之中

### 执行查询
func (db *DB) ExecContext(ctx context.Context, query string, args ...interface{}) (Result, error) {
	var res Result
	var err error
	//最大执行次数,这里是为了防止未获取到连接池,最多尝试两次
	for i := 0; i < maxBadConnRetries; i++ {
	    //执行查询
		res, err = db.exec(ctx, query, args, cachedOrNewConn)
		if err != driver.ErrBadConn {
			break
		}
	}
	//查询返回了连接池错误,再次尝试一次
	if err == driver.ErrBadConn {
		return db.exec(ctx, query, args, alwaysNewConn)
	}
	return res, err
}

### 连接池的获取
func (db *DB) conn(ctx context.Context, strategy connReuseStrategy) (*driverConn, error) {
    //如果已经关闭 直接返回错误
	db.mu.Lock()
	if db.closed {
		db.mu.Unlock()
		return nil, errDBClosed
	}
	// Check if the context is expired.
	//如果上下文返回了cancel()直接返回错误
	select {
	default:
	case <-ctx.Done():
		db.mu.Unlock()
		return nil, ctx.Err()
	}
	//获取连接池最大生存时间
	lifetime := db.maxLifetime

	// Prefer a free connection, if possible.
	numFree := len(db.freeConn)
	//如果空闲链接大于0 并且使用缓存
	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
	}

	// Out of free connections or we were asked not to use one. If we're not
	// allowed to open any more connections, make a request and wait.
	//如果当前链接已经大于最大连接了,需要创建链接
	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.
		//生成一个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 {
		//收到取消channel 从map中删除
		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:
			    //收到添加channel 执行添加操作
				if ok && ret.conn != nil {
					db.putConn(ret.conn, ret.err, false)
				}
			}
			return nil, ctx.Err()
		case ret, ok := <-req:
		    //收到channel关闭请求
			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
		}
	}
    //执行添加操作
	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
}


func (db *DB) putConn(dc *driverConn, err error, resetSession bool) {
	db.mu.Lock()
	//如果是使用的 报错
	if !dc.inUse {
		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 debugGetPut {
		db.lastPut[dc] = stack()
	}
	dc.inUse = false
    
	for _, fn := range dc.onPut {
		fn()
	}
	dc.onPut = nil
    //如果是错误的 maybe一个新的链接
	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
	}
	if putConnHook != nil {
		putConnHook(db, dc)
	}
	if db.closed {
		// Connections do not need to be reset if they will be closed.
		// Prevents writing to resetterCh after the DB has closed.
		resetSession = false
	}
	if resetSession {
		if _, resetSession = dc.ci.(driver.SessionResetter); resetSession {
			// Lock the driverConn here so it isn't released until
			// the connection is reset.
			// The lock must be taken before the connection is put into
			// the pool to prevent it from being taken out before it is reset.
			dc.Lock()
		}
	}
	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:
	}
}


func (db *DB) maybeOpenNewConnections() {
	numRequests := len(db.connRequests)
	if db.maxOpen > 0 {
		numCanOpen := db.maxOpen - db.numOpen
		if numRequests > numCanOpen {
			numRequests = numCanOpen
		}
	}
	for numRequests > 0 {
		db.numOpen++ // optimistically
		numRequests--
		if db.closed {
			return
		}
		//发生一个channel
		db.openerCh <- struct{}{}
	}
}

  1. 启动的两个协程处理了什么
//创建一个新的链接
func (db *DB) connectionOpener(ctx context.Context) {
	for {
		select {
		case <-ctx.Done():
			return
		//收到了channel 执行函数
		case <-db.openerCh:
			db.openNewConnection(ctx)
		}
	}
}
//关闭连接
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)
		}
	}
}

通过两个协程和方法,使连接池维持在一个silice中,需要查询的时候从链接池中去获取

  1. 到底连接池设置多少比较合适
waitCount         int64 // Total number of connections waited for.
maxIdleClosed     int64 // Total number of connections closed due to idle.
maxLifetimeClosed int64 

Db结构体中有这个三个数,我们可以查看这三个数和活动链接的数量,如果关闭的次数太多了话,可以适量的增加连接数,初始值的话,可以设置为核数*2