Goroutines阻止了连接池

时间:2016-07-20 06:43:17

标签: 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()
}

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

如何解决这个问题?

1 个答案:

答案 0 :(得分:4)

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

链接的维基百科文章详细说明了防止死锁。例如,代码执行只应在具有所需(或将需要)的所有资源时才进入关键部分(锁定资源)。在这种情况下,这意味着您必须保留2个连接(正好为2;如果只有1个可用,则保留并等待),如果您有2个,则只进行查询。但在Go中,您无法提前预订连接。执行查询时会根据需要分配它们。

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

一个简单的解决方法是执行第一个查询,保存其结果(例如,进入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")
    }
}()

请注意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循环中,sql.Stmt获取的预准备语句(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")
    }
}()

要让您的程序也等待这些goroutines,请使用您已经拥有的WaitGroup

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