上一章中我们一起探讨了golangdatabase/sql
包中如何获取一个真实的数据库连接。当我们拿到一个数据库连接之后就可以开始真正的数据库操作了。本章讲继续深入,一起探讨底层是如何进行数据库操作的。
上一章中我们说到:
db.Query()
实际上分为两步:
- 获取数据库连接
- 在此连接上利用driver进行实际的DB操作
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.queryConn
其实sql包最核心的就是维护了连接池,对于实际的操作,都是利用Driver去完成。因此代码实现也一样,坚持一个原则:
组装Driver需要的参数,执行Driver的方法
db.queryConn
伪代码如下:
func (db *DB) queryConn(dc *driverConn, releaseConn func(error), query string, args []interface{}) (*Rows, error) {
if queryer, ok := dc.ci.(driver.Queryer); ok {
dargs, err := driverArgs(nil, args)
if err != nil {
releaseConn(err)
return nil, err
}
dc.Lock()
rowsi, err := queryer.Query(query, dargs)
dc.Unlock()
if err != driver.ErrSkip {
if err != nil {
releaseConn(err)
return nil, err
}
// Note: ownership of dc passes to the *Rows, to be freed
// with releaseConn.
rows := &Rows{
dc: dc,
releaseConn: releaseConn,
rowsi: rowsi,
}
return rows, nil
}
}
dc.Lock()
si, err := dc.ci.Prepare(query)
dc.Unlock()
if err != nil {
releaseConn(err)
return nil, err
}
ds := driverStmt{dc, si}
rowsi, err := rowsiFromStatement(ds, args...)
if err != nil {
dc.Lock()
si.Close()
dc.Unlock()
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: si,
}
return rows, nil
}
queryConn的实现可以分为两部分来看:
- Driver实现了
Queryer
接口 - Driver没有实现该接口,走
Stmt三部曲
Queryer
Queryer
接口很能体现golang内部命名interface的风格,比如Reader
、Writer
等,Queryer
要求实现一个Query
方法。如果Driver实现了这个Query
方法,那么sql包只需要把它需要的参数准备好然后传给它就行了。
driverArgs
用来准备Query
需要的参数,实际上就是把各种类型的值利用反射转换成它所在类型的最大类型
。这句话有点不好理解,简单点讲就是把int int8 uint uint16 int16
等转换为int64
,把floatX
转换为float64
。最终,driverArgs
会把所有类型转化为以下几种
[]byte
bool
float64
int64
string
time.Time
思考①:
为什么要进行数据转换
准备好参数之后就调用Driver实现好的Query方法。
dc.Lock()
rowsi, err := queryer.Query(query, dargs)
dc.Unlock()
最终的请求很简单,因为工作量都在driver,但是问题也来了
问题②:
这里为什么要加锁?
每个Query
都会先获取连接再进行Query,如果连接池是线程安全的,对于取到连接的后续行为还需要加锁吗?
调用Driver的Query方法执行完Query请求就拿到了rowsi(Driver.Rows
),将它包一层包成sql.Rows
返回给caller。
// Note: ownership of dc passes to the *Rows, to be freed
// with releaseConn.
rows := &Rows{
dc: dc,
releaseConn: releaseConn,
rowsi: rowsi,
}
return rows, nil
至此呢,一个真实的请求就处理完毕了。实际上对于sql包来说非常简单,工作量都在各种不同的Driver里。
Stmt
正如文档所说,Queryer
接口是可选的:
Queryer is an optional interface that may be implemented by a Conn.
If a Conn does not implement Queryer, the sql package's DB.Query will first prepare a query, execute the statement, and then close the statement.
所以对于那些偷懒的Driver来说,执行一个Query请求就得用Stmt
了。
dc.Lock()
si, err := dc.ci.Prepare(query)
dc.Unlock()
Prepare
方法产生一个Stmt
。当然这里同样有相同的问题需要你思考一下,这里加锁是否有必要。可以先看看Stmt
的定义:
// Stmt is a prepared statement. It is bound to a Conn and not
// used by multiple goroutines concurrently.
type Stmt interface {
// Close closes the statement.
//
// As of Go 1.1, a Stmt will not be closed if it's in use
// by any queries.
Close() error
// NumInput returns the number of placeholder parameters.
//
// If NumInput returns >= 0, the sql package will sanity check
// argument counts from callers and return errors to the caller
// before the statement's Exec or Query methods are called.
//
// NumInput may also return -1, if the driver doesn't know
// its number of placeholders. In that case, the sql package
// will not sanity check Exec or Query argument counts.
NumInput() int
// Exec executes a query that doesn't return rows, such
// as an INSERT or UPDATE.
Exec(args []Value) (Result, error)
// Query executes a query that may return rows, such as a
// SELECT.
Query(args []Value) (Rows, error)
}
可以看到Stmt
的方法也很简单,Exec
和Query
是最终执行请求会需要用到的方法。NumInput
用来统计sql语句中占位符的数量。
很多人之前可能都比较疑惑Stmt
是用来干什么的,看到这里应该明白了。事实上Stmt
就是一个sql语句的模板,模板固定,只是参数在变化,这种场景就特别适合用Stmt
,你不再需要把sql语句复制几遍。
拿到Stmt
之后,通过执行Stmt
的Query
方法,也能拿到结果rows。进行Query之前也需要buildParams以及检查参数和sql语句的placeholder是否匹配等,所以进行了一个简单封装:
ds := driverStmt{dc, si}
rowsi, err := rowsiFromStatement(ds, args...)
si就是Stmt
了为什么还要包成driverStmt
,而driverStmt
又是什么呢?其实主要还是为了在rowsiFromStatement
方法中执行Query
是加锁。参照Queryer
中的代码,执行Query
时是需要加锁的,这把锁是dc提供的,所以包装一个driverStmt
变相让Stmt
有了加锁的方法:
// driverStmt associates a driver.Stmt with the
// *driverConn from which it came, so the driverConn's lock can be
// held during calls.
type driverStmt struct {
sync.Locker // the *driverConn
si driver.Stmt
}
rowsiFromStatement
内部执行完Query后也拿到了Driver.Rows
,如之前一样包装成sql.Rows
返回给caller
就好。
至此,我们已经一起探究了golang的sql包是如何处理Query请求的了。但是还是有一个问题一直贯穿着整个过程,就是:
为什么要加锁
如果只是看Query方法可以还不好理解,但是看了Stmt之后应该就可以理解了。Stmt是可以多次利用的,每个Stmt包含了conn,可以把一个Stmt看成一个数据库连接。有了数据库连接的概念,用户如果在多个goroutine中使用这个Stmt,就会有并发的问题,因此通过Stmt进行Query或者Exec是需要加锁的。
但是对于实现了Queryer
接口的Driver来说,用户调用db.Query
后每次都会取新的连接然后再进行Query,最后返回一个Rows。对用户来说直接Query的整个过程并没有连接的概念,因此我个人觉得是安全的。这里需不需要加锁有待商榷。如果觉得需要加锁欢迎留言和我讨论
Tx
Tx
实际上和上面是一样的,主要也是创建时先请求一个conn,然后基于这个conn包装一个Tx对象。后续的操作都要依赖于底层的数据库。
Tx需要特别注意的是:
如果后端的数据库proxy,就不能使用数据库事务
这和golang无关,所有语言都一样。因为我们无法保证我们对一个事务的请求都落到同一台机器。
关于golang的sql包,到这儿也将告一段落了。其实它的核心就是:
- 维护了数据库连接池
- 定义了一系列接口规范,让Driver可以面向接口进行开发
接下来有时间的话,我写一篇文章来分析go-sql-driver/mysql
,不过底层的实现相对而言会比较无聊,主要都是实现mysql通信协议
的规范,按照规范收发报文。
golang1.8 sql包中新增了不少接口,这很令人期待,更简化了我们对于数据库的使用,方便进行一些高级的封装,而不用层层反射。不过目前各Driver的支持是一个大问题。