美文网首页
dive into golang database/sql(3)

dive into golang database/sql(3)

作者: suoga | 来源:发表于2017-04-06 10:44 被阅读153次

    上一章中我们一起探讨了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)

          本文链接:https://www.haomeiwen.com/subject/llwfattx.html