文章目录

  • Go 与数据库
  • 相关的接口
  • sql.Register 接口
  • driver.Driver 接口
  • driver.Conn 接口
  • driver.Stmt 接口
  • driver.Tx 接口
  • driver.Execer 接口
  • driver.Result 接口
  • driver.Rows 接口
  • driver.RowsAffected
  • driver.Value 接口
  • driver.ValueConverter 接口
  • driver.Valuer 接口
  • database/sql 的介绍
  • 获取连接
  • 释放连接



Go 与数据库


Go 语言没有内置的驱动支持任何的数据库,但是 Go 定义了 database/sql 接口,可以基于驱动接口开发相应数据库的驱动,这样做有一个好处,只要按照标准接口开发的代码, 以后需要迁移数据库时,不需要任何修改。


相关的接口


sql.Register 接口

database/sqlRegister() 函数是用来注册数据库驱动的,当第三方开发者开发数据库驱动时,都会实现 init() 函数,在该函数中会调用这个 Register() 函数来完成驱动的注册,其函数的声明如下:

func Register(name string, driver driver.Driver)

导入包使用 import _ "github.com/go-sql-driver/mysql" ,前面的 _ 作用时不需要把该包都导进来,只执行包的 init() 函数,MySQL 驱动正是通过这种方式注册到 ”database/sql” ,例如如下程序源码部分:

// github.com/go-sql-driver/mysql/driver.go
func init() {
    	sql.Register("mysql", &MySQLDriver{})
}

type MySQLDriver struct{}

func (d MySQLDriver) Open(dsn string) (driver.Conn, error) {
    	...
}

init() 函数通过调用 Register() 函数将 MySQL 驱动添加到 sql.drivers ,第三方数据库驱动都是通过调用这个函数来注册自己的数据库驱动名称以及相应的 driver 实现,在 database/sql 内部通过一个 map 来存储用户定义的相应驱动,其形式如下:

var drivers = make(map[string]driver.Driver)

假如同时用到多种数据库,可以通过调用 sql.Register 将不同数据库的实现注册到 sql.drivers 中,再根据注册的 name 将对应的 driver 取出,因此可以通过 database/sql 的注册函数可以同时注册多个数据库驱动,只要不重复,具体过程参考如下的源码:

// database/sql/sql.go
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
}

关于 init() 函数的初始化过程,包在引入的时候会自动调用包的 init() 函数以完成对包的初始化,因此引入上面的数据库驱动包之后需要手动去调用 init() 函数,然后在 init() 函数里面注册这个数据库驱动,这样就可以在编程中直接使用这个数据库驱动了。

driver.Driver 接口

init() 函数通过 Register() 函数将 MySQL 驱动添加到 sql.drivers (类型:make(map[string]driver.Driver))中, MySQLDriver 实现了 driver.Driver 接口,Driver 是一个数据库驱动的接口,其定义了一个 Open(name string) 方法返回一个数据库的 Conn ,该接口的定义如下:

//database/sql/driver/driver.go
type Driver interface {
    	Open(name string) (Conn, error)
}

返回的 Conn 只能用来进行一次 goroutine 的操作,不能把这个 Conn 应用于 Go 程序的多个 goroutine 里面,例如如下程序代码会出现错误:

...
go goroutineA (Conn) // 执行查询操作

go goroutineB (Conn) // 执行插入操作
...

上面这样的代码可能会使 Go 程序编译运行不知道某个操作究竟是由哪个 goroutine 发起的,从而导致数据混乱(比如可能会把 goroutineA 里面执行的查询操作的结果返回给 goroutineB 从而使 B 错误地把此结果当成自己执行的插入数据)。

第三方驱动都会定义这个函数,它会解析 name 参数来获取相关数据库的连接信息,解析完成后将使用此信息来初始化一个 Conn 并返回它。

driver.Conn 接口

Conn 是一个数据库连接的接口定义,定义了一系列方法,这个 Conn 只能应用在一个 goroutine 里面,不能使用在多个 goroutine 里面,该接口的定义如下:

type Conn interface {
    	Prepare(query string) (Stmt, error)
    	Close() error
    	Begin() (Tx, error)
}
  • Prepare() 函数返回与当前连接相关的执行 SQL 语句的准备状态,可以进行查询、删除等操作。
  • Close() 函数关闭当前的连接,执行释放连接拥有的资源等清理工作。因为驱动实现了 database/sql 里面建议的 conn pool ,所以不用再去实现缓存 Conn 之类的,否则容易引起问题。
  • Begin() 函数返回一个代表事务处理的 Tx ,通过它可以进行查询、更新或者对事务进行回滚、递交等操作。

driver.Stmt 接口

Stmt 是一种准备好的状态和 Conn 相关联,同样只能应用于一个 goroutine 中,不能应用于多个 goroutine ,该接口的定义如下:

type Stmt interface {
    	Close() error
    	NumInput() int
    	Exec(args []Value) (Result, error)
    	Query(args []Value) (Rows, error)
}
  • Close() 函数关闭当前的链接状态,但是如果当前正在执行 queryquery 还是有效返回 Rows 数据 。
  • NumInput() 函数返回当前预留参数的个数,当 返回值 >= 0 时数据库驱动就会智能检查调用者的参数,当数据库驱动包不知道预留参数的时,返回 -1
  • Exec() 函数执行 Prepare 准备好的 SQL ,传入参数执行 update/insert 等操作,返回 Result 数据。
  • Query() 函数执行 Prepare 准备好的 SQL ,传入需要的参数执行 select 操作,返回 Rows 结果集。

driver.Tx 接口

事务处理一般是两个过程(递交或者回滚),数据库驱动里面也只需实现这两个函数即可,Tx 接口的定义如下:

type Tx interface {
    	Commit() error
    	Rollback() error
}

这两个函数一个用来递交一个事务,一个用来回滚事务。

driver.Execer 接口

这是一个 Conn 可选择实现的接口,该接口的定义如下:

type Execer interface {
    	Exec(query string, args []Value) (Result, error)
}

如果这个接口没有定义,那么在调用 DB.Exec() 函数就会首先调用 Prepare() 函数返回 Stmt ,然后执行 StmtExec 关闭 Stmt

driver.Result 接口

这个是执行 Update/Insert 等操作返回的结果的接口,该接口的定义如下:

type Result interface {
    	LastInsertId() (int64, error)
    	RowsAffected() (int64, error)
}
  • LastInsertId() 函数返回由数据库执行插入操作得到的自增 ID 号。
  • RowsAffected() 函数返回 query 操作影响的数据条目数。

driver.Rows 接口

Rows 是执行查询返回的结果集的接口,该接口的定义如下:

type Rows interface {
	    Columns() []string
    	Close() error
   	 	Next(dest []Value) error
}
  • Columns() 函数返回查询数据库表的字段信息,这个返回的 sliceSQL 查询的字段一一对应,而不是返回整个表的所有字段。
  • Close() 函数用来关闭 Rows 迭代器。
  • Next() 函数用来返回下一条数据,把数据赋值给 destdest 里面的元素必须是 driver.Value 的值除了 string ,返回的数据里面所有的 string 都必须要转换成 []byte 类型,如果最后没数据了,Next() 函数最后返回 io.EOF

driver.RowsAffected

RowsAffested 其实就是一个 int64 的别名,它实现了 Result 接口,用来底层实现 Result 的表示方式,具体程序代码如下:

type RowsAffected int64

func (RowsAffected) LastInsertId() (int64, error)

func (v RowsAffected) RowsAffected() (int64, error)

driver.Value 接口

Value 其实是一个空接口,可以容纳任何的数据,过期接口的定义如下:

type Value interface{}

driveValue 是驱动必须能够操作的 ValueValue 要么是 nil 类型,要么是下面的任意一种类型:

int64
float64
bool
[]byte
string [*] // 除了 Rows.Next 返回的不能是 string
time.Time

driver.ValueConverter 接口

ValueConverter 接口定义了如何把一个普通的值转化成 driver.Value 的接口,该接口的定义如下:

type ValueConverter interface {
    	ConvertValue(v interface{}) (Value, error)
}

在开发的数据库驱动包里面实现这个接口的函数在很多地方会使用到,这个 ValueConverter 有很多好处:

  • 转化 driver.Value 到数据库表相应的字段,例如 int64 的数据如何转化成数据库表 uint16 字段。
  • 把数据库查询结果转化成 driver.Value 值。
  • scan() 函数里面把 dirve.Value 值转化成用户定义的值。

driver.Valuer 接口

Valuer 接口定义了返回一个 driver.Value 的方式,该接口的定义如下:

type Valuer interface {
    	Value() (Value, error)
}

很多类型都实现了这个 Value 方法,用来自身与 driver.Value 的转化。

通过理解上面的接口内容,对于驱动的开发有了一个基本的了解,一个驱动只要实现了这些接口就能完成增删查改等基本操作了,剩下的就是与相应的数据库进行数据交互等细节问题了。


database/sql 的介绍


database/sqldatabase/sql/driver 提供的接口基础上定义了一些更高阶的方法,用以简化数据库操作,同时内部还建议性地实现一个 conn pool ,其结构声明如下:

type DB struct {
    driver 			driver.Driver  			// 数据库实现驱动
    dsn    			string  				// 数据库连接、配置参数信息,比如 username、host、password 等
    numClosed 		uint64

    mu           	sync.Mutex          	// 锁,操作 DB 各成员时用到
    freeConn     	[]*driverConn       	// 空闲连接
    connRequests 	[]chan connRequest  	// 阻塞请求队列,等连接数达到最大限制时,后续请求将插入此队列等待可用连接
    numOpen      	int                 	// 已建立连接或等待建立连接数
    openerCh    	chan struct{}        	// 用于 connectionOpener
    closed      	bool
    dep         	map[finalCloser]depSet
    lastPut     	map[*driverConn]string 	// stacktrace of last conn's put; debug only
    maxIdle     	int                     // 最大空闲连接数
    maxOpen     	int                    	// 数据库最大连接数
    maxLifetime time.Duration          		// 连接最长存活期,超过这个时间连接将不再被复用
    cleanerCh   	chan struct{}
}

maxIdle(默认值为 2)、maxOpen(默认值为 0,无限制)、maxLifetime(默认值为 0,永不过期),可以分别通过 SetMaxIdleConnsSetMaxOpenConnsSetConnMaxLifetime 来设定。

可以看到 Open() 函数返回的是 DB 对象,里面有一个 freeConn ,它就是那个简易的连接池,它的实现是当执行 Db.prepare 的时候会 defer db.putConn(ci, err) ,也就是把这个连接放入连接池,每次调用 Conn 的时候会先判断 freeConn 的长度是否大于 0,如果大于 0 说明有可以复用的 Conn ,直接拿出来用;如果不大于 0 ,则创建一个 Conn 再返回之。

这时 MySQL 还没有建立连接,只是初始化了一个 sql.DB 结构,这是非常重要的一个结构,所有相关的数据都保存在此结构中,Open() 函数同时启动了一个 connectionOpener 协程。


获取连接

调用 Open() 函数时是没有建立数据库连接的,只有等用的时候才会实际建立连接,获取可用连接的操作有两种策略:cachedOrNewConn(有可用空闲连接则优先使用,没有则创建)、alwaysNewConn(不管有没有空闲连接都重新创建),下面以一个 query* 的例子看下具体的操作:

rows, err := db.Query("select * from userinfo")

以下是 Query 的源代码:

//database/sql/sql.go:
func (db *DB) Query(query string, args ...interface{}) (*Rows, error) {
    	var rows *Rows
    	var err error
    	// maxBadConnRetries = 2
    	for i := 0; i < maxBadConnRetries; i++ {
        		rows, err = db.query(query, args, cachedOrNewConn)
        		if err != driver.ErrBadConn {
            		break
       			}
    	}
    	if err == driver.ErrBadConn {
        		return db.query(query, args, alwaysNewConn)
    	}
    	return rows, err
}

func (db *DB) query(query string, args []interface{}, strategy connReuseStrategy) (*Rows, error) {
    	ci, err := db.conn(strategy)
    	if err != nil {
        		return nil, err
    	}

    	//到这已经获取到了可用连接,下面进行具体的数据库操作
    	return db.queryConn(ci, ci.releaseConn, query, args)
}

数据库连接由 db.query() 获取,以下是具体的源码:

func (db *DB) conn(strategy connReuseStrategy) (*driverConn, error) {
    	db.mu.Lock()
    	if db.closed {
        		db.mu.Unlock()
        		return nil, errDBClosed
    	}
    	lifetime := db.maxLifetime

    	//从 freeConn 取一个空闲连接
    	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
        		}
        		return conn, nil
    	}

    	//如果没有空闲连接,而且当前建立的连接数已经达到最大限制则将请求加入connRequests队列,
    	//并阻塞在这里,直到其它协程将占用的连接释放或connectionOpenner创建
    	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)
        db.connRequests = append(db.connRequests, req)
        db.mu.Unlock()
        ret, ok := <-req  //阻塞
        if !ok {
            	return nil, errDBClosed
        }
        if ret.err == nil && ret.conn.expired(lifetime) { //连接过期了
            	ret.conn.Close()
            	return nil, driver.ErrBadConn
        }
        	return ret.conn, ret.err
    	}

    	db.numOpen++ //上面说了numOpen是已经建立或即将建立连接数,这里还没有建立连接,只是乐观的认为后面会成功,失败的时候再将此值减1
    	db.mu.Unlock()
    	ci, err := db.driver.Open(db.dsn) //调用driver的Open方法建立连接
    	if err != nil { //创建连接失败
        		db.mu.Lock()
        		db.numOpen-- // correct for earlier optimism
        		db.maybeOpenNewConnections()  //通知connectionOpener协程尝试重新建立连接,否则在db.connRequests中等待的请求将一直阻塞,知道下次有连接建立
        		db.mu.Unlock()
        		return nil, err
    	}
    	db.mu.Lock()
    	dc := &driverConn{
        		db:        db,
        		createdAt: nowFunc(),
        		ci:        ci,
    	}
    	db.addDepLocked(dc, dc)
    	dc.inUse = true
    	db.mu.Unlock()
    	return dc, nil
}

获取连接的过程:

  • 首先检查下 freeConn 里是否有空闲连接,如果有且未超时则直接复用,返回连接;如果没有或连接已经过期则进入下一步。
  • 检查当前已经建立及准备建立的连接数是否已经达到最大值,如果达到最大值也就意味着无法再创建新的连接了,当前请求需要在这等着连接释放,这时当前协程将创建一个 channel:chan connRequest ,并将其插入 db.connRequests 队列,然后阻塞在接收 chan connRequest 上,等到有连接可用时这里将拿到释放的连接,检查可用后返回;如果还未达到最大值则进入下一步。
  • 创建一个连接,首先将 numOpen 加 1,然后再创建连接,如果等到创建完连接再把numOpen 加 1 会导致多个协程同时创建连接时一部分会浪费,所以提前将 numOpen 占用,创建失败再将其减掉;如果创建连接成功则返回连接,失败则进入下一步。
  • 创建连接失败时有一个善后操作,当然并不仅仅是将最初占用的 numOpen 数减掉,更重要的一个操作是通知 connectionOpener 协程根据 db.connRequests 等待的长度创建连接。

善后操作的原因:

  • numOpen 在连接成功创建前就加了 1 ,这时候如果 numOpen 已经达到最大值再有获取 Conn 的请求将阻塞在第二部 ,这些请求会等着先前进来的请求释放连接。
  • 假设先前进来的这些请求创建连接全部失败,那么如果它们直接返回了那些等待的请求将一直阻塞在那,因为不可能有连接释放(极限值,如果部分创建成功则会有部分释放)),直到新请求进来重新成功创建连接,显然这样是有问题的,所以 maybeOpenNewConnections 将通知 connectionOpener 根据 db.connRequests 长度及可创建的最大连接数重新创建连接,然后将新创建的连接发给阻塞的请求。

注:如果 maxOpen=0 将不会有请求阻塞等待连接,所有请求只要从 freeConn 中取不到连接就会新创建。另外 QueryExec 有个重试机制,首先优先使用空闲连接,如果 2 次取到的连接都无效则尝试新创建连接,获取到可用连接后将调用具体数据库的 driver 处理 SQL 。


释放连接

数据库连接在被使用完成后需要归还给连接池以供其它请求复用,释放连接的操作是调用 putConn() 方法,该方法的关键源码部分如下:

func (db *DB) putConn(dc *driverConn, err error) {
    	...

    	//如果连接已经无效,则不再放入连接池
    	if err == driver.ErrBadConn {
        		db.maybeOpenNewConnections()
        		dc.Close() //这里最终将numOpen数减掉
        		return
    	}
    	...

    	//正常归还
    	added := db.putConnDBLocked(dc, nil)
    	...
}

func (db *DB) putConnDBLocked(dc *driverConn, err error) bool {
    	if db.maxOpen > 0 && db.numOpen > db.maxOpen {
        		return false
    	}
    	//有等待连接的请求则将连接发给它们,否则放入freeConn
    	if c := len(db.connRequests); c > 0 {
        		req := db.connRequests[0]
        // This copy is O(n) but in practice faster than a linked list.
        // TODO: consider compacting it down less often and
        // moving the base instead?
        		copy(db.connRequests, db.connRequests[1:])
        		db.connRequests = db.connRequests[:c-1]
        if err == nil {
            	dc.inUse = true
        }
        req <- connRequest{
            	conn: dc,
            	err:  err,
        }
        return true
    	} else if err == nil && !db.closed && db.maxIdleConnsLocked() > len(db.freeConn) {
        		db.freeConn = append(db.freeConn, dc)
        		db.startCleanerLocked()
        		return true
    	}
    	return false
}

释放连接的过程:

  • 首先检查下当前归还的连接在使用过程中是否发现已经无效,如果无效则不再放入连接池,然后检查下等待连接的请求数新建连接,类似获取连接时的异常处理;如果连接有效则进入下一步。
  • 检查下当前是否有等待连接阻塞的请求,有的话将当前连接发给最早的那个请求,没有的话则再判断空闲连接数是否达到上限,没有则放入 freeConn 空闲连接池,达到上限则将连接关闭释放。
  • (只执行一次)启动 connectionCleaner 协程定时检查 feeConn 中是否有过期连接,有则剔除。

有个地方需要注意的是,QueryExec 操作用法有些差异:

a.Exec(update、insert、delete等无结果集返回的操作) 函数调用完后会自动释放连接; b.Query(回sql.Rows) 函数则不会释放连接,调用完后仍然占有连接,它将连接的所属权转移给了 sql.Rows ,所以需要手动调用 Close() 函数归还连接,即使不用 Rows 也得调用 rows.Close() ,否则可能导致后续使用出错,如下的用法是错误的:

//错误
db.SetMaxOpenConns(1)
db.Query("select * from test")

row,err := db.Query("select * from test") //此操作将一直阻塞

//正确
db.SetMaxOpenConns(1)
r,_ := db.Query("select * from test")
r.Close() //将连接的所属权归还,释放连接
row,err := db.Query("select * from test")
//other op
row.Close()

  • 参考书籍:《Go Web 编程》(谢孟军 著)