PostgreSQL
 sql >> Base de données >  >> RDS >> PostgreSQL

Goroutines a bloqué le pool de connexions

Ce que vous avez est un impasse . Dans le pire des cas, vous avez 15 goroutines contenant 15 connexions à la base de données, et toutes ces 15 goroutines nécessitent une nouvelle connexion pour continuer. Mais pour obtenir une nouvelle connexion, il faudrait avancer et libérer une connexion :deadlock.

L'article de wikipedia lié détaille la prévention des blocages. Par exemple, une exécution de code ne doit entrer dans une section critique (qui verrouille les ressources) que lorsqu'elle dispose de toutes les ressources dont elle a besoin (ou aura besoin). Dans ce cas, cela signifie que vous devrez réserver 2 connexions (exactement 2 ; si une seule est disponible, laissez-la et attendez), et si vous avez ces 2 connexions, procédez ensuite aux requêtes. Mais dans Go, vous ne pouvez pas réserver de connexions à l'avance. Ils sont alloués selon les besoins lorsque vous exécutez des requêtes.

Généralement, ce schéma doit être évité. Vous ne devez pas écrire de code qui réserve d'abord une ressource (finie) (connexion à la base de données dans ce cas), et avant de la libérer, il en demande une autre.

Une solution de contournement simple consiste à exécuter la première requête, à enregistrer son résultat (par exemple dans une tranche Go), et lorsque vous avez terminé, passez aux requêtes suivantes (mais n'oubliez pas non plus de fermer sql.Rows première). De cette façon, votre code n'a pas besoin de 2 connexions en même temps.

Et n'oubliez pas de gérer les erreurs ! Je les ai omis par souci de brièveté, mais vous ne devriez pas dans votre code.

Voici à quoi cela pourrait ressembler :

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")
    }
}()

Notez que rows.Close() doit être exécuté comme un defer déclaration pour s'assurer qu'elle sera exécutée (même en cas de panique). Mais si vous utilisez simplement defer rows.Close() , qui ne serait exécuté qu'après l'exécution des requêtes suivantes, il n'empêchera donc pas le blocage. Je le refactoriserais donc pour l'appeler dans une autre fonction (qui peut être une fonction anonyme) dans laquelle vous pouvez utiliser un 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)
        }
    }()

Notez également que dans le deuxième for boucler une instruction préparée (sql.Stmt ) acquis par DB.Prepare() serait probablement un bien meilleur choix pour exécuter la même requête (paramétrée) plusieurs fois.

Une autre option consiste à lancer des requêtes ultérieures dans de nouvelles goroutines afin que la requête exécutée dans celle-ci puisse se produire lorsque la connexion actuellement verrouillée est libérée (ou toute autre connexion verrouillée par une autre goroutine), mais sans synchronisation explicite, vous n'avez pas le contrôle quand ils se font exécuter. Cela pourrait ressembler à ceci :

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")
    }
}()

Pour que votre programme attende également ces goroutines, utilisez le WaitGroup vous avez déjà en action :

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