Remarque
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de vous connecter ou de modifier des répertoires.
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de modifier des répertoires.
Les transactions regroupent plusieurs opérations dans une unité atomique. Soit toutes les opérations réussissent (validation), soit aucune n’est appliquée (annulation). Cet article explique comment utiliser les transactions avec le go-mssqldb pilote, y compris les niveaux d’isolation, la gestion des erreurs et les motifs pour les applications de production.
Les exemples de cet article s’exécutent sur la base de données d’exemple AdventureWorks2025. Les exemples orientés écriture ciblent HumanResources.Department, Production.ProductCategory, Production.ProductSubcategory, et Production.ProductInventory.
Lancer une transaction
Utilisez-le db.BeginTx pour commencer une transaction. La valeur renvoyée *sql.Tx réserve une seule connexion du pool pendant toute la durée de la transaction :
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()
Important
Appelez defer tx.Rollback() toujours immédiatement après BeginTx. Si Commit() réussit, le différé Rollback() est un no-op. Si une erreur survient avant Commit(), l’appel différé à Rollback() garantit ainsi que la transaction ne reste pas ouverte et empêche que la connexion soit renvoyée au pool sans validation.
Niveaux d’isolation
SQL Server prend en charge plusieurs niveaux d’isolation qui contrôlent l’interaction des transactions concurrentes. Fixez le niveau d’isolation dans sql.TxOptions:
tx, err := db.BeginTx(ctx, &sql.TxOptions{
Isolation: sql.LevelReadCommitted,
})
Comparaison des niveaux d’isolation
| Niveau d’isolation | Lectures incorrectes | Lectures non répétables | Lectures fantômes | Impact sur les performances | À utiliser lorsque |
|---|---|---|---|---|---|
sql.LevelReadUncommitted |
Oui | Oui | Oui | Frais généraux les plus bas | Comptages approximatifs, surveillance des tableaux de bord. La précision des données n’est pas cruciale. |
sql.LevelReadCommitted |
Non | Oui | Oui | Default. Convient à la plupart des charges de travail. | Charges de travail générales de l’OLTP. Le point de départ par défaut et recommandé. |
sql.LevelRepeatableRead |
Non | Non | Oui | Modéré. Ça tient les serrures plus longtemps. | Des lectures qui doivent voir des valeurs cohérentes pour les mêmes lignes dans la transaction. |
sql.LevelSerializable |
Non | Non | Non | Le plus élevé. Les verrous de plage bloquent les insertions simultanées. | Transactions financières, gestion des stocks, partout où les lectures fantômes sont inacceptables. |
sql.LevelSnapshot |
Non | Non | Non | Utilise la versionisation par ligne dans tempdb. Pas de blocage. |
Des charges de travail à forte composante de lecture qui nécessitent une cohérence à un instant donné sans bloquer les opérations d’écriture. |
Note
sql.LevelSnapshot nécessite que l’isolation des instantanés soit activée sur la base de données : ALTER DATABASE AdventureWorks2025 SET ALLOW_SNAPSHOT_ISOLATION ON.
Exemple : lire engagé vs. sérialisable
Spécifier le niveau d’isolation dans 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,
})
Routage en lecture seule
Le pilote ne prend pas en charge sql.TxOptions.ReadOnly. Si vous transmettez ReadOnly: true, BeginTx renvoie une erreur.
Pour le routage AlwaysOn en lecture seule, définissez applicationintent=ReadOnly dans la chaîne de connexion lorsque vous ouvrez la connexion :
sqlserver://listener.example.com?database=AdventureWorks2025&applicationintent=ReadOnly
C’est un réglage de niveau connexion. Il peut acheminer les sessions éligibles vers un réplica secondaire lisible, mais ne transforme pas une transaction existante en transaction en lecture seule. Utilisez une connexion dédiée en lecture seule ou des identifiants à privilège minimum pour les charges de travail qui ne doivent pas écrire de données.
Gestion des erreurs dans les transactions
Gérez les erreurs au sein des transactions avec soin. Lorsqu’un relevé échoue, la transaction doit être entièrement annulée. N’essayez pas de continuer avec d’autres affirmations après une erreur :
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
}
Si la procédure batch ou stockée s’exécute avec SET XACT_ABORT ON, considérez toute erreur d’instruction comme un terminal pour la transaction. Revenez immédiatement en arrière et ne tentez pas d’autres déclarations ou Commit(). Les versions actuelles des pilotes détectent les transactions avortées par le serveur et renvoient une erreur au lieu de permettre un commit partiel silencieux.
Points de sauvegarde
Les points de sauvegarde créent des points de retour intermédiaires au sein d’une transaction. SQL Server prend en charge les points de sauvegarde nativement. Puisque le package de database/sql Go n’expose pas directement les points de sauvegarde, exécutez-les en SQL brut via la transaction :
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()
}
Note
SAVE TRANSACTION <name> Crée le point de sauvegarde.
ROLLBACK TRANSACTION <name> Ça revient à ce point de sauvegarde sans mettre fin à la transaction extérieure.
ROLLBACK sans nom annule l’intégralité de la transaction.
Gestion de l'interblocage
SQL Server résout les blocages en terminant l’une des transactions concurrentes (la victime du blocage) et en renvoyant l’erreur 1205. La transaction terminée est automatiquement annulée par le serveur.
Détection et réessai des blocages
Vérifiez l’erreur 1205 et retentez toute la transaction avec un court délai :
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)
}
Utilisez le mécanisme de nouvelle tentative en cas d’interblocage
Passer une fonction de transaction au wrapper de retry :
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
})
Réduire les blocages
| Strategy | Comment cela aide |
|---|---|
| Accéder aux tables dans un ordre cohérent | Lorsque toutes les transactions verrouillent la Table A avant la Table B, les attentes circulaires ne peuvent pas avoir lieu. |
| Gardez les transactions courtes | Les transactions plus courtes conservent les verrous moins longtemps, réduisant ainsi la période propice aux conflits. |
| Utilisez le niveau d’isolation suffisant le plus bas |
ReadCommitted contient moins de verrous que Serializable. |
| Ajouter des index appropriés | Les mises à jour ciblées par index verrouillent moins de lignes que les scans de tables. |
| Évitez l’interaction utilisateur lors des transactions | N’attendez jamais l’entrée utilisateur entre BeginTx et Commit. |
Réessayer est la réponse correcte dans le code applicatif, mais des blocages répétés sur la même requête indiquent un problème de conception. Utilisez le graphique de blocage de SQL Server (capturé via les événements étendus ou la session de santé système) pour identifier les instructions concurrentes et les types de verrous, puis appliquez les stratégies du tableau précédent. Pour un aperçu complet de l’analyse et de la prévention des blocages, consultez le guide des blocages.
Transactions et maintien de la connexion
Une transaction immobilise une seule connexion du pool jusqu’à ce Commit() que ou Rollback() soit appelée. Pendant ce temps, aucune autre goroutine ne peut utiliser cette connexion.
Implications :
- Les transactions de longue durée réduisent la taille effective du pool. Si vous avez
MaxOpenConns=25et 20 transactions ouvertes, il ne reste que 5 connexions disponibles pour d’autres tâches. - Un
Rollback()oublié provoque une fuite de connexion permanente. - L’annulation de contexte sur le contexte de la transaction annule la transaction et renvoie la connexion au 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()
Transactions concurrentes
Chaque goroutine doit créer sa propre transaction. Ne partagez jamais un *sql.Tx entre plusieurs goroutines, car *sql.Tx n’est pas sûr en cas d’utilisation concurrente :
// 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()
Transactions distribuées
Le pilote go-mssqldb ne prend pas en charge les transactions distribuées (transactions XA ou leurs équivalents System.Transactions). Si vous devez coordonner le travail sur plusieurs bases de données :
- Utilise un motif saga avec des actions compensatrices.
- Consolider les opérations dans une seule base de données lorsque c’est possible.
- Utilisez des serveurs liés avec
BEGIN DISTRIBUTED TRANSACTIONfrom Transact-SQL (T-SQL) si les deux bases de données sont SQL Server.
Liste de contrôle des transactions
| Area | Recommendation |
|---|---|
| Sécurité de recul | Toujours defer tx.Rollback() immédiatement après BeginTx. |
| Niveau d’isolation | Commencez par ReadCommitted (par défaut). Escaladez uniquement si besoin. |
| Interblocages | Enroulez le code transactionnel dans une boucle de réessayage. Accédez aux tables dans un ordre cohérent. |
| Durée | Gardez les transactions aussi courtes que possible. Fixez des échéances contextuelles. |
| Concurrency | Ne partagez jamais un *sql.Tx entre plusieurs goroutines. |
| Points de sauvegarde | Utilisation SAVE TRANSACTION et ROLLBACK TRANSACTION <name> pour un retour en arrière partiel. |
| Impact du bassin | Surveillez db.Stats().InUse pour détecter les fuites de connexion dues à des transactions non engagées. |