Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
Le transazioni raggruppano più operazioni in un'unità atomica. O tutte le operazioni vanno a buon fine (commit) oppure nessuna di esse produce effetti (rollback). Questo articolo spiega come utilizzare le transazioni con il go-mssqldb driver, inclusi livelli di isolamento, gestione degli errori e pattern per applicazioni di produzione.
Gli esempi in questo articolo vengono confrontati con il database di esempio AdventureWorks2025 . Gli esempi orientati alla scrittura sono destinati a HumanResources.Department, Production.ProductCategory, Production.ProductSubcategory e Production.ProductInventory.
Avvia una transazione
Usalo db.BeginTx per iniziare una transazione. L'elemento restituito *sql.Tx vincola una singola connessione dal pool di connessioni per tutta la durata della transazione:
tx, err := db.BeginTx(ctx, nil) // nil uses the default isolation level
if err != nil {
return err
}
defer tx.Rollback() // No-op if tx.Commit() succeeds first.
_, err = tx.ExecContext(ctx, "INSERT INTO HumanResources.Department (Name, GroupName) VALUES (@p1, @p2)",
sql.Named("p1", name),
sql.Named("p2", groupName))
if err != nil {
return err
}
return tx.Commit()
Importante
Chiama defer tx.Rollback() sempre subito dopo BeginTx. Se Commit() ha esito positivo, la chiamata differita a Rollback() non produce alcun effetto. Se si verifica un errore prima di Commit(), l'istruzione defer Rollback() garantisce che la transazione non rimanga aperta e che la connessione non venga restituita al pool in uno stato non confermato.
Livelli di isolamento
SQL Server supporta diversi livelli di isolamento che controllano come interagiscono le transazioni concorrenti. Imposta il livello di isolamento in sql.TxOptions:
tx, err := db.BeginTx(ctx, &sql.TxOptions{
Isolation: sql.LevelReadCommitted,
})
Confronto dei livelli di isolamento
| Livello di isolamento | Letture sporche | Letture non ripetibili | Letture fantasma | Impatto sulle prestazioni | Usa quando |
|---|---|---|---|---|---|
sql.LevelReadUncommitted |
Yes | Yes | Yes | Costi generali più bassi | Conteggi approssimativi, dashboard di monitoraggio. L'accuratezza dei dati non è fondamentale. |
sql.LevelReadCommitted |
No | Yes | Yes | Predefinito Buono per la maggior parte dei carichi di lavoro. | Carichi di lavoro generali OLTP. Il punto di partenza predefinito e consigliato. |
sql.LevelRepeatableRead |
No | No | Yes | Moderato. Mantiene i blocchi più a lungo. | Letture che devono vedere valori coerenti per le stesse righe all'interno della transazione. |
sql.LevelSerializable |
No | No | No | Sommo. I blocchi di intervallo impediscono gli inserimenti simultanei. | Transazioni finanziarie, gestione dell'inventario, ovunque le letture fantasma sono inaccettabili. |
sql.LevelSnapshot |
No | No | No | Utilizza il versioning delle righe in tempdb. Niente blocchi. |
Carichi di lavoro con molta lettura che richiedono coerenza nel momento preciso senza bloccare gli scrittori. |
Annotazioni
sql.LevelSnapshot richiede che l'isolamento snapshot sia attivato nel database: ALTER DATABASE AdventureWorks2025 SET ALLOW_SNAPSHOT_ISOLATION ON.
Esempio: lettura confermata vs. serializzabile
Specifica il livello di isolamento in sql.TxOptions:
// Read Committed (default) - suitable for most operations.
tx1, err := db.BeginTx(ctx, &sql.TxOptions{
Isolation: sql.LevelReadCommitted,
})
// Serializable - prevents phantom reads in financial calculations.
tx2, err := db.BeginTx(ctx, &sql.TxOptions{
Isolation: sql.LevelSerializable,
})
Routing di sola lettura
Il driver non supporta sql.TxOptions.ReadOnly. Se superi ReadOnly: true, BeginTx restituisce un errore.
Per il routing di sola lettura di AlwaysOn, imposta applicationintent=ReadOnly nella stringa di connessione quando si apre la connessione:
sqlserver://listener.example.com?database=AdventureWorks2025&applicationintent=ReadOnly
Questa è un'impostazione a livello di connessione. Può instradare le sessioni idonee verso un secondario leggibile, ma non trasforma una transazione esistente in una transazione di sola lettura. Usa una connessione dedicata di sola lettura o credenziali con privilegio minimo per carichi di lavoro che non devono scrivere dati.
Gestione degli errori nelle transazioni
Gestisci con attenzione gli errori all'interno delle transazioni. Se una qualsiasi istruzione fallisce, la transazione deve essere annullata completamente. Non tentare di continuare con altre affermazioni dopo un errore:
func createCategory(ctx context.Context, db *sql.DB, category Category) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("begin transaction: %w", err)
}
defer tx.Rollback()
var categoryId int64
err = tx.QueryRowContext(ctx, `
INSERT INTO Production.ProductCategory (Name)
OUTPUT INSERTED.ProductCategoryID
VALUES (@name)`,
sql.Named("name", category.Name)).Scan(&categoryId)
if err != nil {
return fmt.Errorf("insert category: %w", err)
}
// Insert subcategories under the new category.
for _, sub := range category.Subcategories {
_, err = tx.ExecContext(ctx, `
INSERT INTO Production.ProductSubcategory (ProductCategoryID, Name)
VALUES (@catId, @name)`,
sql.Named("catId", categoryId),
sql.Named("name", sub.Name))
if err != nil {
return fmt.Errorf("insert subcategory %s: %w", sub.Name, err)
}
}
if err = tx.Commit(); err != nil {
return fmt.Errorf("commit category: %w", err)
}
return nil
}
Se il batch o la procedura archiviata viene eseguito con SET XACT_ABORT ON, considera qualsiasi errore in un'istruzione come fatale per la transazione. Torna indietro immediatamente e non tentare altre affermazioni o Commit(). Le attuali versioni dei driver rilevano transazioni abortite dal server e restituiscono un errore invece di consentire un commit parziale silenzioso.
Punti di salvataggio
I punti di salvataggio creano punti di rollback intermedi all'interno di una transazione. SQL Server supporta i punti di salvataggio nativamente. Poiché il pacchetto di database/sql Go non espone direttamente i punti di salvataggio, eseguili come SQL grezzo attraverso la transazione:
func createCategoryWithOptionalSubcategory(ctx context.Context, db *sql.DB, category Category) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
err = tx.QueryRowContext(ctx,
"INSERT INTO Production.ProductCategory (Name) OUTPUT INSERTED.ProductCategoryID VALUES (@p1)",
sql.Named("p1", category.Name)).Scan(&category.Id)
if err != nil {
return err
}
// Try to add a subcategory. If it fails, roll back only the subcategory part.
_, err = tx.ExecContext(ctx, "SAVE TRANSACTION AddSubcategory")
if err != nil {
return err
}
_, err = tx.ExecContext(ctx,
"INSERT INTO Production.ProductSubcategory (ProductCategoryID, Name) VALUES (@p1, @p2)",
sql.Named("p1", category.Id),
sql.Named("p2", category.DefaultSubcategory))
if err != nil {
// Roll back only the subcategory; the category insert is preserved.
_, rbErr := tx.ExecContext(ctx, "ROLLBACK TRANSACTION AddSubcategory")
if rbErr != nil {
return fmt.Errorf("rollback savepoint: %w (original: %w)", rbErr, err)
}
log.Printf("Subcategory %q failed, proceeding without it: %v",
category.DefaultSubcategory, err)
}
return tx.Commit()
}
Annotazioni
SAVE TRANSACTION <name> crea il punto di salvataggio.
ROLLBACK TRANSACTION <name> torna indietro a quel punto di salvataggio senza terminare la transazione esterna.
ROLLBACK senza nome annulla l'intera transazione.
Gestione dello stallo
SQL Server risolve i deadlock terminando una delle transazioni concorrenti (la vittima dello deadlock) e restituendo l'errore 1205. La transazione terminata viene automaticamente annullata dal server.
Rilevare e riprovare in caso di situazioni di stallo
Controlla l'errore 1205 e riprova l'intera transazione con un breve ritardo:
import (
"errors"
"fmt"
"time"
"github.com/microsoft/go-mssqldb"
)
func isDeadlock(err error) bool {
var mssqlErr mssql.Error
return errors.As(err, &mssqlErr) && mssqlErr.Number == 1205
}
func withDeadlockRetry(ctx context.Context, db *sql.DB, maxRetries int,
fn func(ctx context.Context, tx *sql.Tx) error) error {
for attempt := 0; attempt < maxRetries; attempt++ {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
err = fn(ctx, tx)
if err != nil {
tx.Rollback()
if isDeadlock(err) && attempt < maxRetries-1 {
// Wait briefly before retrying.
delay := time.Duration(attempt+1) * 50 * time.Millisecond
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(delay):
}
continue
}
return err
}
if err = tx.Commit(); err != nil {
if isDeadlock(err) && attempt < maxRetries-1 {
delay := time.Duration(attempt+1) * 50 * time.Millisecond
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(delay):
}
continue
}
return err
}
return nil
}
return fmt.Errorf("transaction failed after %d deadlock retries", maxRetries)
}
Usa il wrapper per i tentativi in caso di deadlock
Passare una funzione di transazione al wrapper di ripetizione:
err := withDeadlockRetry(ctx, db, 3, func(ctx context.Context, tx *sql.Tx) error {
_, err := tx.ExecContext(ctx,
"UPDATE Production.ProductInventory SET Quantity = Quantity - @qty WHERE ProductID = @pid AND LocationID = @lid",
sql.Named("qty", orderQty),
sql.Named("pid", productId),
sql.Named("lid", locationId))
return err
})
Ridurre gli stalli
| Strategy | In che modo può essere utile |
|---|---|
| Tabelle di accesso in ordine coerente | Quando tutte le transazioni bloccano la Tabella A prima della Tabella B, non possono verificarsi attese circolari. |
| Mantieni le transazioni brevi | Transazioni più brevi trattengono i blocchi per meno tempo, riducendo la finestra di conflitto. |
| Usa il livello di isolamento sufficiente più basso |
ReadCommitted ha meno blocchi di Serializable. |
| Aggiungi indici appropriati | Gli aggiornamenti targetizzati all'indice bloccano meno righe rispetto alle scansioni delle tabelle. |
| Evita l'interazione dell'utente durante le transazioni | Non aspettare mai l'input dell'utente tra BeginTx e Commit. |
Ritentare è la risposta corretta nel codice applicativo, ma i deadlock ripetuti sulla stessa query indicano un problema di progettazione. Usa il grafo dei deadlock di SQL Server (acquisito tramite Eventi estesi o la sessione integrità del sistema) per identificare le istruzioni in conflitto e i tipi di blocco, quindi applica le strategie riportate nella tabella precedente. Per una guida completa sull'analisi e prevenzione degli sblocchi, consulta la guida agli sblocchi.
Transazioni e blocco della connessione
Una transazione mantiene occupata una singola connessione del pool finché non viene chiamato Commit() o Rollback(). Durante questo periodo, nessun'altra goroutine può usare quella connessione.
Implicazioni:
- Le transazioni di lunga durata riducono la dimensione effettiva del pool. Se hai
MaxOpenConns=25e 20 transazioni aperte, solo 5 connessioni sono disponibili per altre attività. - La mancata chiusura di
Rollback()provoca una perdita permanente della connessione. - La cancellazione del contesto sul contesto della transazione annulla la transazione e restituisce la connessione al pool.
// Set a deadline to prevent transactions from running indefinitely.
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
Transazioni concorrenti
Ogni goroutine dovrebbe creare la propria transazione. Non condividere mai un *sql.Tx fra più goroutine perché *sql.Tx non è sicuro per l'uso concorrente:
// CORRECT: Each goroutine gets its own transaction.
var g errgroup.Group
for _, item := range items {
item := item
g.Go(func() error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
_, err = tx.ExecContext(ctx,
"UPDATE Production.ProductInventory SET Quantity = Quantity - 1 WHERE ProductID = @p1 AND LocationID = @p2",
sql.Named("p1", item.ProductId),
sql.Named("p2", item.LocationId))
if err != nil {
return err
}
return tx.Commit()
})
}
return g.Wait()
Transazioni distribuite
Il go-mssqldb driver non supporta transazioni distribuite (transazioni XA o System.Transactions equivalenti). Se devi coordinare il lavoro su più database:
- Usa il modello saga con azioni compensative.
- Consolidare le operazioni in un unico database quando possibile.
- Usare server collegati con
BEGIN DISTRIBUTED TRANSACTIONda Transact-SQL (T-SQL) se entrambi i database sono database SQL Server.
Elenco di controllo delle transazioni
| Area | Raccomandazione |
|---|---|
| Sicurezza del rollback | Sempre defer tx.Rollback() subito dopo BeginTx. |
| Livello di isolamento | Inizia con ReadCommitted (il predefinito). Escalare solo quando necessario. |
| Deadlock | Racchiudi il codice transazionale in un ciclo di nuovo tentativo. Accedi alle tabelle in un ordine coerente. |
| Durata | Mantieni le transazioni il più brevi possibile. Stabilisci scadenze contestuali. |
| Concurrency | Non condividere mai *sql.Tx tra goroutine. |
| Punti di salvataggio | Uso SAVE TRANSACTION e ROLLBACK TRANSACTION <name> per un rollback parziale. |
| Impatto sulla piscina | Monitora db.Stats().InUse per rilevare perdite di connessione dovute a transazioni non compromette. |