PostgreSQL
 sql >> Database >  >> RDS >> PostgreSQL

Goroutine ha bloccato il pool di connessioni

Quello che hai è un deadlock . Nel peggiore dei casi hai 15 goroutine che contengono 15 connessioni al database e tutte queste 15 goroutine richiedono una nuova connessione per continuare. Ma per ottenere una nuova connessione, si dovrebbe avanzare e rilasciare una connessione:deadlock.

L'articolo di Wikipedia collegato descrive in dettaglio la prevenzione dello stallo. Ad esempio, un'esecuzione di codice dovrebbe entrare in una sezione critica (che blocca le risorse) solo quando ha tutte le risorse di cui ha bisogno (o avrà bisogno). In questo caso questo significa che dovresti prenotare 2 connessioni (esattamente 2; se solo 1 è disponibile, lasciala e aspetta), e se hai quelle 2, solo allora procedi con le query. Ma in Go non puoi prenotare i collegamenti in anticipo. Vengono allocati in base alle esigenze quando esegui le query.

Generalmente questo schema dovrebbe essere evitato. Non dovresti scrivere codice che prima riserva una risorsa (finita) (connessione db in questo caso) e prima che la rilasci, ne richiede un'altra.

Una soluzione semplice è eseguire la prima query, salvarne il risultato (ad es. in una fetta Go) e, quando hai finito, procedere con le query successive (ma non dimenticare di chiudere sql.Rows primo). In questo modo il tuo codice non ha bisogno di 2 connessioni contemporaneamente.

E non dimenticare di gestire gli errori! Li ho omessi per brevità, ma non dovresti nel tuo codice.

Ecco come potrebbe apparire:

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

Nota che rows.Close() dovrebbe essere eseguito come defer istruzione per assicurarsi che venga eseguita (anche in caso di panico). Ma se usi semplicemente defer rows.Close() , che verrebbe eseguito solo dopo l'esecuzione delle query successive, quindi non impedirà il deadlock. Quindi lo refactoring per chiamarlo in un'altra funzione (che potrebbe essere una funzione anonima) in cui puoi usare 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)
        }
    }()

Nota anche che nel secondo for loop una dichiarazione preparata (sql.Stmt ) acquisito da DB.Prepare() sarebbe probabilmente una scelta molto migliore per eseguire la stessa query (parametrizzata) più volte.

Un'altra opzione è lanciare query successive in nuove goroutine in modo che la query eseguita in quella possa verificarsi quando viene rilasciata la connessione attualmente bloccata (o qualsiasi altra connessione bloccata da qualsiasi altra goroutine), ma quindi senza sincronizzazione esplicita non hai il controllo quando vengono giustiziati. Potrebbe assomigliare a questo:

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

Per fare in modo che il tuo programma attenda anche queste goroutine, usa il WaitGroup hai già in azione:

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