Goroutines阻止了连接池

Eri*_*ric 5 postgresql go goroutine

package main

import (
    "database/sql"
    "fmt"
    _ "github.com/lib/pq"
    "sync"
)

func main() {
    db, _ := sql.Open("postgres", fmt.Sprintf("host=%s dbname=%s user=%s sslmode=disable", "localhost", "dbname", "postgres"))
    defer db.Close()

    db.SetMaxOpenConns(15)
    var wg sync.WaitGroup
    for i := 0; i < 15; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            //#1
            rows, _ := db.Query("SELECT * FROM reviews LIMIT 1")
            for rows.Next() {
                //#2
                db.Exec("SELECT * FROM reviews LIMIT 1")
            }
        }()
    }

    wg.Wait()
}
Run Code Online (Sandbox Code Playgroud)

查询1打开15个连接,rows.Next()执行时将关闭它们。但是rows.Next()永远不会执行,因为它包含db.Exec()等待自由连接的内容。

如何解决这个问题呢?

icz*_*cza 6

你所拥有的是一个僵局。在最坏的情况下,你有 15 个 goroutines 持有 15 个数据库连接,所有这 15 个 goroutines 都需要一个新的连接才能继续。但是要获得一个新的连接,就必须推进和释放一个连接:死锁。

链接的维基百科文章详细介绍了死锁的预防。例如,代码执行应该只在它拥有所需(或将需要)的所有资源时进入临界区(锁定资源)。在这种情况下,这意味着您必须保留 2 个连接(正好是 2 个;如果只有 1 个可用,则保留它并等待),如果您有这 2 个,则才继续进行查询。但是在 Go 中,您不能提前预订连接。当您执行查询时,它们会根据需要进行分配。

通常应该避免这种模式。您不应该编写首先保留(有限)资源(在本例中为 db 连接)的代码,并且在释放它之前,它需要另一个资源。

一个简单的解决方法是执行第一个查询,保存其结果(例如,保存到 Go 切片中),当您完成后,然后继续后续查询(但也不要忘记先关闭sql.Rows)。这样您的代码就不需要同时进行 2 个连接。

并且不要忘记处理错误!为简洁起见,我省略了它们,但您不应该在代码中。

这是它的样子:

go func() {
    defer wg.Done()

    rows, _ := db.Query("SELECT * FROM reviews LIMIT 1")
    var data []int // Use whatever type describes data you query
    for rows.Next() {
        var something int
        rows.Scan(&something)
        data = append(data, something)
    }
    rows.Close()

    for _, v := range data {
        // You may use v as a query parameter if needed
        db.Exec("SELECT * FROM reviews LIMIT 1")
    }
}()
Run Code Online (Sandbox Code Playgroud)

请注意,rows.Close()应作为defer语句执行以确保它会被执行(即使在发生恐慌的情况下)。但是如果你只是简单地使用defer rows.Close(),那只会在执行后续查询之后执行,所以它不会防止死锁。所以我会重构它以在另一个函数(可能是匿名函数)中调用它,您可以在其中使用defer

    rows, _ := db.Query("SELECT * FROM reviews LIMIT 1")
    var data []int // Use whatever type describes data you query
    func() {
        defer rows.Close()
        for rows.Next() {
            var something int
            rows.Scan(&something)
            data = append(data, something)
        }
    }()
Run Code Online (Sandbox Code Playgroud)

另请注意,在第二个for循环中,sql.Stmt由 by 获取的准备好的语句 ( )DB.Prepare()可能是多次执行相同(参数化)查询的更好选择。

另一种选择是在新的 goroutine 中启动后续查询,以便在释放当前锁定的连接(或任何其他 goroutine 锁定的任何其他连接)时执行的查询可以发生,但是如果没有显式同步,您将无法控制何时他们被处决。它可能看起来像这样:

go func() {
    defer wg.Done()

    rows, _ := db.Query("SELECT * FROM reviews LIMIT 1")
    defer rows.Close()
    for rows.Next() {
        var something int
        rows.Scan(&something)
        // Pass something if needed
        go db.Exec("SELECT * FROM reviews LIMIT 1")
    }
}()
Run Code Online (Sandbox Code Playgroud)

为了让你的程序也等待这些 goroutines,使用WaitGroup你已经在行动中的:

        // Pass something if needed
        wg.Add(1)
        go func() {
            defer wg.Done()
            db.Exec("SELECT * FROM reviews LIMIT 1")
        }()
Run Code Online (Sandbox Code Playgroud)