北京猫爷爷 發表於 2019-8-25 17:28:00

Go组件学习——database/sql数据库连接池你用对了吗

<p><span style="color: rgba(51, 102, 255, 1)"><strong><span style="font-size: 18pt; font-family: &quot;Microsoft YaHei&quot;">1、案例</span></strong></span></p>
<p><strong><span style="font-size: 14pt; font-family: &quot;Microsoft YaHei&quot;">case1: maxOpenConns &gt; 1</span></strong></p>
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">func fewConns() {
        db, _ := db.Open("mysql", "root:rootroot@/dqm?charset=utf8&amp;parseTime=True&amp;loc=Local")

        db.SetMaxOpenConns(10)
        rows, err := db.Query("select * from test where name = 'jackie' limit 10")
        if err != nil {
                fmt.Println("query error")
        }

        row, _ := db.Query("select * from test")
        fmt.Println(row, rows)
}</pre>
</div>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">这里maxOpenConns设置为10,足够这里的两次查询使用了。</span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">程序正常执行并结束,打印了一堆没有处理的结果,如下:</span></p>
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">&amp;{0xc0000fc180 0x10bbb80 0xc000106050 &lt;nil&gt; &lt;nil&gt; {{0 0} 0 0 0 0} false &lt;nil&gt; []} &amp;{0xc0000f4000 0x10bbb80 0xc0000f8000 &lt;nil&gt; &lt;nil&gt; {{0 0} 0 0 0 0} false &lt;nil&gt; []}
</pre>
</div>
<p>  </p>
<p><strong><span style="font-size: 14pt; font-family: &quot;Microsoft YaHei&quot;">case2: maxOpenConns = 1</span></strong></p>
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">func oneConn() {
        db, _ := db.Open("mysql", "root:rootroot@/dqm?charset=utf8&amp;parseTime=True&amp;loc=Local")

        db.SetMaxOpenConns(1)
        rows, err := db.Query("select * from test where name = 'jackie' limit 10")
        if err != nil {
                fmt.Println("query error")
        }

        row, _ := db.Query("select * from test")
        fmt.Println(row, rows)
}</pre>
</div>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">这里maxOpenConns设置为1,但是这里有两次查询,需要两个连接,通过调试发现一直阻塞在</span></p>
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">row, _ := db.Query("select * from test")</pre>
</div>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">之所以阻塞,是因为拿不到连接,可用的连接一直被上一次查询占用了。</span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">&nbsp;</span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">执行结果如下图所示</span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;"><img src="https://img2018.cnblogs.com/blog/619240/201908/619240-20190825170701295-1720313469.png" alt=""></span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">&nbsp;</span></p>
<p><span style="font-size: 14pt"><strong><span style="font-family: &quot;Microsoft YaHei&quot;">case3: maxOpenConns = 1 + for rows.Next()</span></strong></span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">通过case2发现可能会存在连接泄露的情况,所以继续保持maxOpenConns=1</span></p>
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">func oneConnWithRowsNext() {
        db, _ := db.Open("mysql", "root:rootroot@/dqm?charset=utf8&amp;parseTime=True&amp;loc=Local")

        db.SetMaxOpenConns(1)
        rows, err := db.Query("select * from test where name = 'jackie' limit 10")
        if err != nil {
                fmt.Println("query error")
        }

        for rows.Next() {
                fmt.Println("close")
        }

        row, _ := db.Query("select * from test")
        fmt.Println(row, rows)
}</pre>
</div>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">除了maxOpenConns=1以外,这里多了rows遍历的代码。</span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">&nbsp;</span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">执行结果如下</span></p>
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">close
close
close
close
close
close
&amp;{0xc000104000 0x10bbfe0 0xc0000e40f0 &lt;nil&gt; &lt;nil&gt; {{0 0} 0 0 0 0} false &lt;nil&gt; []} &amp;{0xc000104000 0x10bbfe0 0xc0000e40a0 &lt;nil&gt; &lt;nil&gt; {{0 0} 0 0 0 0} true 0xc00008e050 [ ]}
</pre>
</div>
<p>  </p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">显然,这里第二次查询并没有阻塞,而是拿到了连接并查到了结果。</span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">所以,这里rows遍历一定帮我们做了一些有关获取连接的事情,后面展开。</span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">&nbsp;</span></p>
<p><strong><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">case4: maxOpenConns = 1 + for rows.Next() + 异常退出</span></strong></p>
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">func oneConnWithRowsNextWithError() {
        db, _ := db.Open("mysql", "root:rootroot@/dqm?charset=utf8&amp;parseTime=True&amp;loc=Local")

        db.SetMaxOpenConns(1)
        rows, err := db.Query("select * from test where name = 'jackie' limit 10")
        if err != nil {
                fmt.Println("query error")
        }

        i := 1
        for rows.Next() {
                i++
                if i == 3 {
                        break
                }
                fmt.Println("close")
        }

        row, _ := db.Query("select * from test")
        fmt.Println(row, rows)
}</pre>
</div>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">case3中添加了rows的遍历代码,可以让下一次查询拿到连接,那我们继续考察,如果在rows遍历的过程中发生了以外提前退出了,是否影响后面sql语句的执行。</span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">&nbsp;</span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">执行结果如下图所示</span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;"><img src="https://img2018.cnblogs.com/blog/619240/201908/619240-20190825171123655-1381077955.png" alt=""></span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">可以看出rows遍历的提前结束,影响了后面查询,出现了和case2同样的情况,即拿不到数据库连接,一直阻塞。</span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">&nbsp;</span></p>
<p><span style="font-size: 14pt"><strong><span style="font-family: &quot;Microsoft YaHei&quot;">case5: maxOpenConns = 1 + for rows.Next() + 异常退出 + rows.Close()</span></strong></span></p>
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">func oneConnWithRowsNextWithErrorWithRowsClose() {
        db, _ := db.Open("mysql", "root:rootroot@/dqm?charset=utf8&amp;parseTime=True&amp;loc=Local")

        db.SetMaxOpenConns(1)
        rows, err := db.Query("select * from test where name = 'jackie' limit 10")
        if err != nil {
                fmt.Println("query error")
        }

        i := 1
        for rows.Next() {
                i++
                if i == 3 {
                        break
                }
                fmt.Println("close")
        }
        rows.Close()


        row, _ := db.Query("select * from test")
        fmt.Println(row, rows)
}</pre>
</div>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">case4是不是就没救了,只能一直阻塞在第二次查询了?</span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">看上面的代码,在异常退出后,我们调用了关闭rows的语句,继续执行第二次查询。</span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">&nbsp;</span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">执行结果如下</span></p>
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">close
&amp;{0xc00010c000 0x10f0ab0 0xc0000e80a0 &lt;nil&gt; &lt;nil&gt; {{0 0} 0 0 0 0} false &lt;nil&gt; []} &amp;{0xc00010c000 0x10f0ab0 0xc0000e8050 &lt;nil&gt; &lt;nil&gt; {{0 0} 0 0 0 0} true &lt;nil&gt; [ ]}</pre>
</div>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">这次,从执行结果看,第二次查询正常执行,并没有阻塞。</span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">&nbsp;</span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">所以,这是为什么呢?</span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">下面先看看database/sql的连接池是如何实现的</span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">&nbsp;</span></p>
<p><span style="color: rgba(51, 102, 255, 1)"><strong><span style="font-size: 18pt; font-family: &quot;Microsoft YaHei&quot;">2、database/sql的连接池</span></strong></span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">网上关于database/sql连接池的实现有很多介绍文章。</span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">其中gorm这样的orm框架的数据库连接池也是复用database/sql的连接池。</span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">大致分为四步</span></p>
<p><strong><span style="font-size: 14pt; font-family: &quot;Microsoft YaHei&quot;">第一步:驱动注册</span></strong></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">我们提供下上面几个case所在的main函数代码</span></p>
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">package main

import (
        db "database/sql"
        "fmt"
        //_ "github.com/jinzhu/gorm/dialects/mysql"
        _ "github.com/go-sql-driver/mysql"
)

func main() {
        // maxConn &gt; 1
        fewConns()
        // maxConn = 1
        oneConn()

        // maxConn = 1 + for rows.Next()
        oneConnWithRowsNext()
        // maxConn = 1 + for rows.Next() + 提前退出
        oneConnWithRowsNextWithError()
        // maxConn = 1 + for rows.Next() + 提前退出 + defer rows.Close()
        oneConnWithRowsNextWithErrorWithRowsClose()
}</pre>
</div>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">这里说的驱动注册就是指</span></p>
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">_ "github.com/go-sql-driver/mysql"</pre>
</div>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">也可以使用gorm中的MySQL驱动注册即</span></p>
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">_ "github.com/jinzhu/gorm/dialects/mysql"</pre>
</div>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">驱动注册主要是注册不同的数据源,比如MySQL、PostgreSQL等</span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">&nbsp;</span></p>
<p><strong><span style="font-size: 14pt; font-family: &quot;Microsoft YaHei&quot;">第二步:初始化DB</span></strong></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">初始化DB即调用Open函数,这时候其实没有真的去获取DB操作的连接,只是初始化得到一个DB的数据结构。</span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">&nbsp;</span></p>
<p><strong><span style="font-size: 14pt; font-family: &quot;Microsoft YaHei&quot;">第三步:获取连接</span></strong></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">获取连接是在具体的sql语句中执行的,比如Query方法、Exec方法等。</span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">以Query方法为例,可以一直追踪源码实现,源码实现路径如下</span></p>
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">sql.go(Query()) -&gt; sql.go(QueryContext()) -&gt; sql.go(query()) -&gt; sql.go(conn())</pre>
</div>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">进入conn()方法的具体实现逻辑是如果连接池中有空闲的连接且没有过期的就直接拿出来用;</span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">如果当前实际连接数已经超过最大连接数即上面case中提到的maxOpenConns,则将任务添加到任务队列中等待;</span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">以上情况都不满足,则自行创建一个新的连接用于执行DB操作。</span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">&nbsp;</span></p>
<p><span style="font-size: 14pt"><strong><span style="font-family: &quot;Microsoft YaHei&quot;">第四步:释放连接</span></strong></span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">当DB操作结束后,需要将连接释放,比如放回到连接池中,以便下一次DB操作的使用。</span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">释放连接的代码实现在sql.go中的putConn()方法。</span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">其主要做的工作是判定连接是否过期,如果没有过期则放回连接池。</span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">&nbsp;</span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">连接池的完整实现逻辑如下图所示</span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;"><img src="https://img2018.cnblogs.com/blog/619240/201908/619240-20190825172407794-1529172865.jpg" alt=""></span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">&nbsp;</span></p>
<p><span style="color: rgba(51, 102, 255, 1)"><strong><span style="font-size: 18pt; font-family: &quot;Microsoft YaHei&quot;">3、案例分析</span></strong></span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">有了前面的背景知识,我们来分析下上面5个case</span></p>
<p><strong><span style="font-size: 14pt; font-family: &quot;Microsoft YaHei&quot;">case1</span></strong></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">最大连接数为10个,代码中只有两个查询任务,完全可以创建两个连接执行。</span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">&nbsp;</span></p>
<p><strong><span style="font-size: 14pt; font-family: &quot;Microsoft YaHei&quot;">case2</span></strong></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">最大连接数为1个,第一次查询已经占用。第二次查询之所以阻塞是因为第一次查询完成后没有释放连接,又因为最大连接数只能是1的限制,导致第二次查询拿不到连接。</span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">&nbsp;</span></p>
<p><strong><span style="font-size: 14pt; font-family: &quot;Microsoft YaHei&quot;">case3 </span></strong></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">最大连接数为1个,但是在第一次查询完成后,调用了rows遍历代码。通过源码可以知道rows遍历代码</span></p>
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">func (rs *Rows) Next() bool {
        var doClose, ok bool
        withLock(rs.closemu.RLocker(), func() {
                doClose, ok = rs.nextLocked()
        })
        if doClose {
                rs.Close()
        }
        return ok
}
</pre>
</div>
<p>  </p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">rows遍历会在最后一次遍历的时候调用rows.Close()方法,该方法会释放连接。</span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">所以case3的链接是在rows遍历中释放的</span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">&nbsp;</span></p>
<p><strong><span style="font-size: 14pt; font-family: &quot;Microsoft YaHei&quot;">case4</span></strong></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">最大连接数为1个,也用了rows遍历,但是连接仍然没有释放。</span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">case3中已经说明过,在最后一次遍历才会调用rows.Close()方法,因为这里的rows遍历中途退出了,导致释放连接的代码没有执行到。所以第二次查询依然阻塞,拿不到连接。</span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">&nbsp;</span></p>
<p><strong><span style="font-size: 14pt; font-family: &quot;Microsoft YaHei&quot;">case5</span></strong></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">最大连接数为1个,使用了rows遍历,且中途以外退出,但是主动调用了rows.Close(),等价于rows遍历完整执行,即释放了连接,所以第二次查询拿到连接正常执行查询任务。</span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">注意:在实际开发中,我们更多使用的是下面的优雅方式</span></p>
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">defer rows.Close()
</pre>
</div>
<p>  </p>
<p><span style="color: rgba(51, 102, 255, 1)"><strong><span style="font-size: 18pt; font-family: &quot;Microsoft YaHei&quot;">4、心得体会</span></strong></span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">最近本来是在看gorm的源码,也想过把gorm应用到我们的项目组里,但是因为一些二次开发以及性能问题,上马gorm的计划先搁置了。</span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">然后在看到gorm代码的时候发现很多地方还是直接使用了database/sql,尤其是连接池这块的实现。</span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">在看这块代码的时候,还发现了我们项目的部分代码中使用了rows遍历,但是忘记添加defer rows.Close()的情况。这种情况一般不会有什么问题,但是如果因为一些意外情况导致提前退出遍历,则可能会出现连接泄露的问题。</span></p>
<p>&nbsp;</p>
<p>如果您觉得阅读本文对您有帮助,请点一下“<strong>推荐</strong>”按钮,您的<strong>“推荐”</strong>将是我最大的写作动力!如果您想持续关注我的文章,请扫描二维码,关注JackieZheng的微信公众号,我会将我的文章推送给您,并和您一起分享我日常阅读过的优质文章。</p>
<pre><em><img src="https://images0.cnblogs.com/blog2015/619240/201505/162205410643708.jpg" alt=""></em></pre><br><br>
来源:https://www.cnblogs.com/bigdataZJ/p/database-sql-connection-pool.html
頁: [1]
查看完整版本: Go组件学习——database/sql数据库连接池你用对了吗