惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

WordPress大学
WordPress大学
H
Help Net Security
Jina AI
Jina AI
V
V2EX
G
Google Developers Blog
B
Blog
GbyAI
GbyAI
U
Unit 42
爱范儿
爱范儿
腾讯CDC
Engineering at Meta
Engineering at Meta
酷 壳 – CoolShell
酷 壳 – CoolShell
博客园 - 三生石上(FineUI控件)
宝玉的分享
宝玉的分享
小众软件
小众软件
D
DataBreaches.Net
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
博客园 - Franky
博客园 - 聂微东
The Cloudflare Blog
I
InfoQ
Microsoft Azure Blog
Microsoft Azure Blog
Hugging Face - Blog
Hugging Face - Blog
大猫的无限游戏
大猫的无限游戏

博客园 - 立志做一个好的程序员

【转】Golang使用时区时候报错 详解 Go 中的 rune 类型 【转】扫盲:Windows桌面应用开发框架:原生、跨平台、云桌面 Go-Zero自定义goctl实战:定制化模板,加速你的微服务开发效率 Go单体服务开发最佳实践 【转】Androidstudio报错Algorithm HmacPBESHA256 not available 借助ai来分析代码,理解代码 【转】linux设置密钥登录(只允许密钥登录) 【转】QPS和并发数的关系 【转】Command Pattern in Go (Golang) [转]Golang Functional Options Pattern 【转】Raft 理论基础 【转】spin lock 和mutex [转]Go sync.Once:简约而不简单的并发利器 [转]Golang atomic.CompareAndSwapInt64()实例讲解 [转]手把手教你如何用golang实现一个timewheel时间轮 [转]Golang线程池实现百万级高并发 [转]Go网络编程 · 一条TCP连接讲透九大知识点 【转】,接上面3篇.Implement Sql Database Driver in 100 Lines of Go
【转】dive into golang database/sql(3)
立志做一个好的程序员 · 2023-10-17 · via 博客园 - 立志做一个好的程序员

转,原文: https://www.jianshu.com/p/cd8cee3d7fc3

----------------

上一章中我们一起探讨了golangdatabase/sql包中如何获取一个真实的数据库连接。当我们拿到一个数据库连接之后就可以开始真正的数据库操作了。本章讲继续深入,一起探讨底层是如何进行数据库操作的。

上一章中我们说到:

实际上分为两步:

  • 获取数据库连接
  • 在此连接上利用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
            }
            

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 {
    

可以看到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 

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

作者:suoga
链接:https://www.jianshu.com/p/cd8cee3d7fc3
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。