dive into golang database/sql(3)

上一章中我们一起探讨了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的风格,比如ReaderWriter等,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的方法也很简单,ExecQuery是最终执行请求会需要用到的方法。NumInput用来统计sql语句中占位符的数量。

很多人之前可能都比较疑惑Stmt是用来干什么的,看到这里应该明白了。事实上Stmt就是一个sql语句的模板,模板固定,只是参数在变化,这种场景就特别适合用Stmt,你不再需要把sql语句复制几遍。

拿到Stmt之后,通过执行StmtQuery方法,也能拿到结果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的支持是一个大问题

你可能感兴趣的:(dive into golang database/sql(3))