前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >亲身经历的痛--database/sql: Stmt的使用以及坑

亲身经历的痛--database/sql: Stmt的使用以及坑

作者头像
李海彬
发布2018-03-20 16:39:23
6.6K1
发布2018-03-20 16:39:23
举报
文章被收录于专栏:Golang语言社区Golang语言社区

前言

众所周知,golang操作数据库,是通过database/sql包,以及第三方的实现了database/sql/driver接口的数据库驱动包来共同完成的。

其中database/sql/driver中的接口Conn和Stmt,官方交给第三方实现驱动,并且是协程不安全的。官方实现的database/sql包中的DB和Stmt是协程安全的,因为内部实现是连接池。

如何使用

刚开始接触database/sql包,并且网上的很多使用例子,都类似下面这种方式:

代码语言:javascript
复制
db,err := sql.Open("mysql","test:test@tcp(127.0.0.1:3306)/abwork?charset=utf8")
rows,err := db.Query("select id,name,age from test limit 0,5")
res, err := db.Exec("INSERT test SET name=?,age =?", "xiaowei", 18)

或者下面这种:

代码语言:javascript
复制
stmt,err := db.Prepare("insert into test(name,age)values(?,?)")
defer stmt.Close()
stmt.Exec("张三",20)

其实仔细看database/sql的源码实现,上面两个例子的方式都是一样的。 首先看一下第一种方式的使用,以下是golang1.3版本database/sql包sql.go中,type DB的exec方法,第899-906行的实现:

代码语言:javascript
复制
dc.Lock()
si, err := dc.ci.Prepare(query)
dc.Unlock()
if err != nil {
    return nil, err
}
defer withLock(dc, func() { si.Close() })
return resultFromStatement(driverStmt{dc, si}, args...)

可以看到,dc.ci.Prepare(query)这句,会先创建一个预处理语句,然后调用resultFromStatement方法执行sql操作。而Query方法,最终的实现也是一样。

我们再看第二种方式,先调用Papare方法生成一个Stmt,在Prepare中,会调用dc.prepareLocked(query),请看sql.go中844行,而最终dc.prepareLocked(query)这个方法,还是会调用dc.ci.Prepare(query)创建预处理语句,请看251行。接下来,就是调用Stmt的Exec或者Query方法,而最终这两个方法还是会调用resultFromStatement方法去执行。

那么两种方式的相通之处,都是会预处理,不同的是使用db.Prepare会额外的创建Stmt,由Stmt实例在去处理具体的数据库操作。

那么大家也看出来了,如果不是批量的操作,是没必要使用db.Papare方法的,否则即多了Stmt创建和关闭的性能开销,又多写了两行代码,有点得不偿失。如果是批量的操作,那么毋庸置疑,肯定是db.Papare拿到Stmt,再由Stmt去执行sql,这样保证批量操作只进行一次预处理。

发现的问题

按照上文说的,在实际使用过程中,也发现了一些问题。

DB默认的最大open连接数是0,而最大空闲数是defaultMaxIdleConns = 2。在数据库操作很频繁的实际使用场景中,尤其是一波又一波访问高峰不间断来临的时候,数据库性能会不断的消耗在连接的创建和销毁上,这是很拖累数据和和机器的,所以我们根据mysql的max_user_connections参数,设置合理的值之后,这种现象很快的稳定下来。

而连接稳定,在使用db.Exec和Query时,每次都会向数据库创建预处理语句。虽说在批量操作时使用db.Papare拿到Stmt然后执行批量操作更好,但我们的实际业务场景中,涉及到批量操作的地方很少(可以说没有),那么问题来了。。。不是挖掘机哪家强,而是该如何合理的使用Stmt!

众所周知,我们的手游《暖暖环游世界》非常适合广大妹纸和宅男朋友们暖身解闷,ios版请点击此处下载,android版请点击此处下载。所以数据流量也非常的大。在大推期间,访问量不断的刷新最高值,mysql的压力爆表,经过分析,除了刚开始最大open连接数和最大空闲连接数设置不当,导致mysql连接方面极大的性能开销外,还有就是这第二个问题,不断的创建预处理语句又关闭销毁,也是性能杀手。(暖暖推广期间毛老师丁丁刘做了大量的mysql优化,详情请看《mysql在高内存、IO利用率上的几个优化点》)

虽说批量操作不多,但我们所有的操作都是相同的sql语句,没有在用户使用过程中,根据用户不同行为,生成不同sql语句的业务场景。

所以我们决定改变使用方式,直接在程序初始化的时候,通过db.Papare创建Stmt,在运行期间,总是由预先创建好的Stmt去执行sql,这样就可以达到预处理语句复用的效果,应该能够很大的减少mysql预处理语句上的性能开销。

而且Stmt的源码实现,也是使用的DB连接池,管理着一批已经创建预处理语句的连接,具体就是下面的struct:

代码语言:javascript
复制
type connStmt struct {
    dc *driverConn
    si driver.Stmt
}

可以看见,每一个连接driverConn对应着一个预处理driver.Stmt。

而Stmt在执行Exec和Query等方法时,会先执行connStmt方法(注意,这是及其重要的一个方法):

代码语言:javascript
复制
func (s *Stmt) connStmt() (ci *driverConn, releaseConn func(error), si driver.Stmt, err error)

拿到type connStmt struct中的driverConn和driver.Stmt,再由resultFromStatement方法去做具体的数据库操作。

Stmt的坑

上文所说的方法,经过实际使用,确实会降低mysql在预处理语句上的性能开销,但问题来了,Stmt的坑是啥呢。。。

回到connStmt方法,Stmt通过该方法获得driverConn和driver.Stmt,那么具体的逻辑是咋样的呢,请看以下源码分析:

代码语言:javascript
复制
func (s *Stmt) connStmt() (ci *driverConn, releaseConn func(error), si driver.Stmt, err error) {
    if err = s.stickyErr; err != nil {
        return
    }
    s.mu.Lock()
    if s.closed {
        s.mu.Unlock()
        err = errors.New("sql: statement is closed")
        return
    }

    // In a transaction, we always use the connection that the
    // transaction was created on.
    if s.tx != nil { // 这里如果有事务,则通过事务tx获取连接(不是本文重点)
        s.mu.Unlock()
        ci, err = s.tx.grabConn() // blocks, waiting for the connection.
        if err != nil {
            return
        }
        releaseConn = func(error) {}
        return ci, releaseConn, s.txsi.si, nil
    }
        // 重点开始
    var cs connStmt
    match := false
    for i := 0; i < len(s.css); i++ { // 如果你翻开Stmt源码,就会发现css是type connStmt的一个slice:css []connStmt
        v := s.css[i]
        _, err := s.db.connIfFree(v.dc) // Stmt首先会从css中找到当前空闲的连接
        if err == nil {
            match = true // 如果有空闲连接,将match设为true
            cs = v
            break
        }
        if err == errConnClosed { // 顺便还处理下关闭的连接,从css中remove掉
            // Lazily remove dead conn from our freelist.
            s.css[i] = s.css[len(s.css)-1]
            s.css = s.css[:len(s.css)-1]
            i--
        }

    }
    s.mu.Unlock()

    // Make a new conn if all are busy.
    // TODO(bradfitz): or wait for one? make configurable later?
    if !match { // 注意match,如果css中没有空闲连接
        dc, err := s.db.conn() // 就会从db的连接池中取
        if err != nil {
            return nil, nil, nil, err
        }
        dc.Lock()
        si, err := dc.prepareLocked(s.query) // 取到连接,这里加把锁去创建预处理语句(注意,这里是重点!!!)
        dc.Unlock()
        if err != nil {
            s.db.putConn(dc, err) // 将连接放到池中
            return nil, nil, nil, err
        }        s.mu.Lock()
        cs = connStmt{dc, si} // 创建connStmt
        s.css = append(s.css, cs) // 将connStmt加到css
        s.mu.Unlock()
    }

    conn := cs.dc
    return conn, conn.releaseConn, cs.si, nil
}

看完上面的源码,相信大家还是没太明白问题出在哪。

我们先明确一件事,那就是Stmt中css里的driveConn,和db连接池中的conn,有可能数量不同,状态不同,有可能db连接池中被干掉的连接,也有可能还在css中。

更有可能,s.db.connIfFree(v.dc)时还不是空闲的连接,在执行到if !match 时,已经是了,然后被dc, err := s.db.conn()从连接池中取出,接下来就到了源码分析中,重点的那句,那里不管连接有木有处于css中,都会创建一个新的connStmt,然后再存入css中。这么一来,导致的问题就是不断有driverConn创建新的driver.Stmt,然后存入css却不销毁。同一个driverConn,有可能会对应着成千上万不同的driver.Stmt。

而我们之所以会发现这个坑,就是在观察mysql状态时,发现Prepared_stmt_count的值,会随着时间的推移,不断的上涨,直到达到max_prepared_stmt_count的最大值,然后代码报错。而这个时候,我们只能通过杀掉进程重启,使Prepared_stmt_count归0,来缓解这个问题。

后续

发现这个问题后,我们分别在: https://code.google.com/p/go/issues/detail?id=8376 https://groups.google.com/forum/#!topic/golang-nuts/bbFX0qQvsB0 这两个地方提出了问题,然后也得到了热心朋友的支持。

ma…@joh.to(找不到对方的真实邮箱,有问题的朋友可以在golang-nuts,通过回复发送邮件联系)帮我们在: https://codereview.appspot.com/116930043 给官方提了codereview和临时解决方案,遗憾的是他的解决方案依然存在这个问题。

最终,database/sql的作者,Brad Fitzpatrick大神亲自回复,明确了问题,然后在: https://code.google.com/p/go/source/detail?r=fdb52a28028a 里做了解决,并且在go1.4中会得到修复。

以下是go1.4beta1中,Brad Fitzpatrick大神的修改,可以对比下旧的实现:

代码语言:javascript
复制
func (s *Stmt) connStmt() (ci *driverConn, releaseConn func(error), si driver.Stmt, err error) {
    if err = s.stickyErr; err != nil {
        return
    }
    s.mu.Lock()
    if s.closed {
        s.mu.Unlock()
        err = errors.New("sql: statement is closed")
        return
    }

    // In a transaction, we always use the connection that the
    // transaction was created on.
    if s.tx != nil {
        s.mu.Unlock()
        ci, err = s.tx.grabConn() // blocks, waiting for the connection.
        if err != nil {
            return
        }
        releaseConn = func(error) {}        return ci, releaseConn, s.txsi.si, nil
    }

    for i := 0; i < len(s.css); i++ {
        v := s.css[i]
        _, err := s.db.connIfFree(v.dc)
        if err == nil {
            s.mu.Unlock()
            return v.dc, v.dc.releaseConn, v.si, nil
        }
        if err == errConnClosed {            // Lazily remove dead conn from our freelist.
            s.css[i] = s.css[len(s.css)-1]
            s.css = s.css[:len(s.css)-1]
            i--
        }

    }
    s.mu.Unlock()

    // If all connections are busy, either wait for one to become available (if
    // we've already hit the maximum number of open connections) or create a
    // new one.
    //
    // TODO(bradfitz): or always wait for one? make configurable later?
    dc, err := s.db.conn()
    if err != nil {
        return nil, nil, nil, err
    }

    // Do another pass over the list to see whether this statement has
    // already been prepared on the connection assigned to us.
    s.mu.Lock()
    for _, v := range s.css { // 这里又做了一次css的检查,看连接是否已经预处理过了
        if v.dc == dc {
            s.mu.Unlock()
            return dc, dc.releaseConn, v.si, nil
        }
    }
    s.mu.Unlock()

    // No luck; we need to prepare the statement on this connection
    dc.Lock()
    si, err = dc.prepareLocked(s.query)
    dc.Unlock()
    if err != nil {
        s.db.putConn(dc, err)
        return nil, nil, nil, err
    }
    s.mu.Lock()
    cs := connStmt{dc, si}
    s.css = append(s.css, cs)
    s.mu.Unlock()

    return dc, dc.releaseConn, si, nil
}
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2016-03-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Golang语言社区 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 如何使用
  • 发现的问题
  • Stmt的坑
  • 后续
相关产品与服务
数据库
云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档