【问题标题】:Goroutines blocked connection poolGoroutines 阻塞了连接池
【发布时间】:2016-07-20 06:43:17
【问题描述】:
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()
}

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

如何解决这个问题?

【问题讨论】:

  • 您是否尝试过在单独的 goroutine 中启动 Exec?例如go db.Exec("SELECT * FROM reviews LIMIT 1")?

标签: postgresql go goroutine


【解决方案1】:

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

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

通常应该避免这种模式。您不应该编写首先保留(有限)资源(在这种情况下为数据库连接)的代码,然后在释放它之前,它需要另一个。

一个简单的解决方法是执行第一个查询,保存其结果(例如保存到 Go 切片中),当你完成后,然后继续执行后续查询(但也不要忘记关闭 @987654322 @ 第一的)。这样您的代码就不需要同时进行 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")
    }
}()

请注意,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)
        }
    }()

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

另一种选择是在新的 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")
    }
}()

要让您的程序也等待这些 goroutine,请使用您已经在运行的 WaitGroup

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

【讨论】:

  • 第一个解决方案(使用 Go 切片)使用太多内存,因为 Query #1 在实际应用程序中返回多于 1 行。 go db.Exec() 为我工作。谢谢!
  • @Eric 即使查询 #1 返回超过 1 行,它可能会使用更少的内存来将结果存储在一个切片中,这与为每一行启动一个新的 goroutine(这也需要内存)相反。
  • @Eric 另请注意,如果您使用第一个解决方案(将结果存储在切片中),您还可以利用使用准备好的语句。请参阅编辑后的答案。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-11-05
  • 1970-01-01
  • 2017-04-18
  • 2014-06-07
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多