Transaktioner med go-mssqldb

Transaktioner grupperar flera operationer i en atomär enhet. Antingen lyckas alla operationer (commit) eller så träder ingen av dem i kraft (rollback). Den här artikeln täcker hur man använder transaktioner med drivrutinen go-mssqldb , inklusive isoleringsnivåer, felhantering och mönster för produktionsapplikationer.

Exempel i denna artikel körs mot AdventureWorks2025 :s exempeldatabas. Skrivorienterade exempel riktar sig mot HumanResources.Department, Production.ProductCategory, Production.ProductSubcategory, och Production.ProductInventory.

Starta en transaktion

Använd db.BeginTx för att starta en transaktion. Det returnerade *sql.Tx låser en enda anslutning från poolen under transaktionens livstid:

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

Ring alltid defer tx.Rollback() direkt efter BeginTx. Om Commit() lyckas är den uppskjutna Rollback() en no-op. Om något fel uppstår före Commit(), säkerställer det uppskjutna anropet till Rollback() att transaktionen inte förblir öppen och att anslutningen inte läcker tillbaka till poolen utan att ha commitats.

Isoleringsnivåer

SQL Server stöder flera isoleringsnivåer som styr hur samtidiga transaktioner interagerar. Sätt isoleringsnivån i sql.TxOptions:

tx, err := db.BeginTx(ctx, &sql.TxOptions{
    Isolation: sql.LevelReadCommitted,
})

Jämförelse av isoleringsnivåer

Isoleringsnivå Smutsiga läsningar Icke-repeterbara läsningar Fantomläsningar Prestandapåverkan Använd när
sql.LevelReadUncommitted Yes Yes Yes Lägsta omkostnader Ungefärliga antal, övervakningspaneler. Datanoggrannhet är inte avgörande.
sql.LevelReadCommitted No Yes Yes Standardinställning. Bra för de flesta arbetsbelastningar. Allmänna OLTP-arbetsbelastningar. Den standard- och rekommenderade startpunkten.
sql.LevelRepeatableRead No No Yes Moderat. Håller låsen längre. Läsningar som måste se konsekventa värden för samma rader inom transaktionen.
sql.LevelSerializable No No No Högsta. Räckviddslås blockerar samtidiga insatser. Finansiella transaktioner, lagerhantering – överallt där fantomläsningar är oacceptabla.
sql.LevelSnapshot No No No Använder radversionering i tempdb. Ingen blockering. Läsintensiva arbetsbelastningar som kräver tidpunktskonsistens utan att blockera skrivande processer.

Note

sql.LevelSnapshot kräver att snapshot-isolering aktiveras i databasen: ALTER DATABASE AdventureWorks2025 SET ALLOW_SNAPSHOT_ISOLATION ON.

Exempel: Read committed jämfört med serializable

Specificera isoleringsnivån i 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,
})

Skrivskyddad routning

Drivrutinen stöder inte sql.TxOptions.ReadOnly. Om du skickar in ReadOnly: true returnerar BeginTx ett fel.

För AlwaysOn skrivskyddad routning, sätt applicationintent=ReadOnly i reťazec pripojenia när du öppnar anslutningen:

sqlserver://listener.example.com?database=AdventureWorks2025&applicationintent=ReadOnly

Detta är en anslutningsnivå-inställning. Den kan routa berättigade sessioner till en läsbar sekundär, men gör inte en befintlig transaktion skrivskyddad. Använd en dedikerad skrivskyddsanslutning eller inloggningsuppgifter med lägsta möjliga behörighet för arbetsbelastningar som inte ska kunna skriva data.

Felhantering i transaktioner

Hantera fel i transaktioner med försiktighet. När ett kontoutdrag misslyckas måste transaktionen återställas helt. Försök inte fortsätta med andra påståenden efter ett fel:

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
}

Om batch- eller lagrad procedur körs med SET XACT_ABORT ON, behandlas alla satsfel som terminal för transaktionen. Rulla tillbaka omedelbart och försök inte fler uttalanden eller Commit(). Nuvarande drivrutinsversioner upptäcker transaktioner som avbrutits av servern och returnerar ett felmeddelande i stället för att tillåta en tyst partiell bekräftelse.

Spara poäng

Savepoints skapar mellanliggande återställningspunkter i en transaktion. SQL Server stöder savepoints nativt. Eftersom Gos database/sql paket inte exponerar sparpunkter direkt, kör dem som rå SQL via transaktionen:

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> skapar sparpunkten. ROLLBACK TRANSACTION <name> återgår till den återställningspunkten utan att avsluta den omgivande transaktionen. ROLLBACK Utan namn rullas hela transaktionen tillbaka.

Hantering av deadlock

SQL Server löser deadlocks genom att avsluta en av de konkurrerande transaktionerna (deadlock-offret) och returnera felmeddelande 1205. Den avslutade transaktionen rullas automatiskt tillbaka av servern.

Identifiera och försök igen vid dödlägen

Kontrollera för fel 1205 och försök hela transaktionen igen med kort fördröjning:

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

Använd omslutningen för återförsök vid deadlock

Skicka en transaktionsfunktion till wrappern för återförsök:

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

Minska antalet deadlock

Strategy Så här blir det enklare
Åtkomsttabeller i konsekvent ordning När alla transaktioner låser Tabell A före Tabell B kan cirkulära väntetider inte inträffa.
Håll transaktionerna korta Kortare transaktioner håller lås kortare tid och minskar tidsfönstret för konflikter.
Använd den lägsta tillräckliga isoleringsnivån ReadCommitted Har färre lås än Serializable.
Lägg till lämpliga index Indexriktade uppdateringar låser färre rader än tabellskanningar.
Undvik användarinteraktion under transaktioner Vänta aldrig på användarinput mellan BeginTx och Commit.

Att försöka igen är rätt svar i applikationskoden, men upprepade deadlocks på samma fråga indikerar ett designproblem. Använd SQL Server deadlock-grafen (fångad via Extended Events eller systemets hälsosession) för att identifiera konkurrerande satser och låstyper, och tillämpa sedan strategierna i föregående tabell. För en fullständig genomgång av deadlock-analys och förebyggande, se Deadlocks-guiden.

Transaktioner och låsning av anslutningar

En transaktion låser en enskild anslutning från poolen tills Commit() eller Rollback() anropas. Under den tiden kan ingen annan goroutine använda den anslutningen.

Konsekvenser:

  • Långvariga transaktioner minskar den effektiva poolstorleken. Om du har MaxOpenConns=25 och 20 öppna transaktioner finns bara 5 anslutningar tillgängliga för annat arbete.
  • En glömd Rollback() leder till en permanent anslutningsläcka.
  • Kontextavbrytning på transaktionens kontext rullar tillbaka transaktionen och returnerar anslutningen till poolen.
// 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()

Samtidiga transaktioner

Varje goroutine bör skapa sin egen transaktion. Dela aldrig en *sql.Tx mellan gorutiner eftersom *sql.Tx inte är säker för samtidig användning:

// 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()

Distribuerade transaktioner

Drivrutinen go-mssqldb stöder inte distribuerade transaktioner (XA-transaktioner eller System.Transactions motsvarande). Om du behöver samordna arbete över flera databaser:

  • Använd ett sagamönster med kompenserande handlingar.
  • Konsolidera verksamheten i en enda databas när det är möjligt.
  • Använd länkade servrar med BEGIN DISTRIBUTED TRANSACTION från Transact-SQL (T-SQL) om båda databaserna är SQL Server.

Transaktionschecklista

Area Recommendation
Rollback-säkerhet Alltid defer tx.Rollback() omedelbart efter BeginTx.
Isoleringsnivå Börja med ReadCommitted (standarden). Eskalera bara när det behövs.
Deadlocks Omslut transaktionskod i en återförsöksloop. Öppna tabeller i en konsekvent ordning.
Varaktighet Håll transaktionerna så korta som möjligt. Sätt tidsgränser för kontext.
Concurrency Dela aldrig en *sql.Tx mellan goroutines.
Spara poäng Använd SAVE TRANSACTION och ROLLBACK TRANSACTION <name> för partiell återställning.
Poolpåverkan Övervaka db.Stats().InUse för att upptäcka anslutningsläckor från oavslutade transaktioner.