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的支持是一个大问题

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,802评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,109评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,683评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,458评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,452评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,505评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,901评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,550评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,763评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,556评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,629评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,330评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,898评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,897评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,140评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,807评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,339评论 2 342

推荐阅读更多精彩内容