Transakce s go-mssqldb

Transakce seskupují více operací do jedné atomové jednotky. Buď všechny operace uspějí (commit), nebo se neprojeví účinek žádné z nich (rollback). Tento článek se zabývá tím, jak používat transakce s ovladačem go-mssqldb , včetně úrovní izolace, zpracování chyb a vzorů pro produkční aplikace.

Příklady v tomto článku jsou porovnány s databází AdventureWorks2025 . Příklady zaměřené na zápis jsou určeny pro HumanResources.Department, Production.ProductCategory, Production.ProductSubcategory a Production.ProductInventory.

Zahajte transakci

Použijte db.BeginTx k zahájení transakce. Vrácené *sql.Tx piny představují jedno spojení z poolu po celou dobu trvání transakce:

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

Vždy volejte defer tx.Rollback() ihned po BeginTx. Pokud Commit() uspěje, odložené Rollback() je no-op. Pokud před Commit() dojde k nějaké chybě, odložené volání Rollback() zajistí, že transakce nezůstane otevřená a připojení se nevrátí do poolu v nepotvrzeném stavu.

Úrovně izolace

SQL Server podporuje několik úrovní izolace, které řídí, jak současně transakce interagují. Nastavte izolační úroveň v sql.TxOptions:

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

Porovnání úrovní izolace

Úroveň izolace Zašpiněná čtení Neopakovatelné čtení Fantomové čtení Dopad na výkon Použít, když
sql.LevelReadUncommitted Yes Yes Yes Nejnižší režie Přibližné počty, monitorovací dashboardy. Přesnost dat není kritická.
sql.LevelReadCommitted Ne Yes Yes Výchozí. Dobré pro většinu pracovních zátěží. Obecné OLTP pracovní zátěže. Výchozí a doporučený výchozí bod.
sql.LevelRepeatableRead Ne Ne Yes Střední. Drží zámky déle. Operace čtení, které musí v rámci transakce vidět konzistentní hodnoty pro stejné řádky.
sql.LevelSerializable Ne Ne Ne Nejvyšší. Zámky rozsahu blokují souběžné vkládání. Finanční transakce, správa zásob, všude, kde jsou falešné čtení nepřijatelné.
sql.LevelSnapshot Ne Ne Ne Používá verzování řádků v tempdb. Žádné blokování. Pracovní zátěže s velkým množstvím čtení, které potřebují konzistenci v jednotlivých momentech, aniž by blokovaly autory.

Note

sql.LevelSnapshot Vyžaduje zapnutí izolace snímků v databázi: ALTER DATABASE AdventureWorks2025 SET ALLOW_SNAPSHOT_ISOLATION ON.

Příklad: Read committed vs. serializable

Specifikujte úroveň izolace v 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,
})

Směrování pouze pro čtení

Ovladač nepodporuje sql.TxOptions.ReadOnly. Pokud předáte ReadOnly: true, BeginTx vrátí chybu.

Pro směrování pouze pro čtení AlwaysOn nastavte applicationintent=ReadOnly v připojovací řetězec při otevření spojení:

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

Jedná se o nastavení na úrovni připojení. Může směrovat způsobilé relace do čitelné sekundární databáze, ale nezpůsobí, že existující transakce je pouze pro čtení. Používejte dedikované připojení pouze pro čtení nebo přihlašovací údaje s nejmenšími oprávněními pro pracovní zátěže, která nesmí zapisovat data.

Zpracování chyb v transakcích

Pečlivě řešte chyby v transakcích. Když nějaký výpis selže, musí být transakce zcela vrácena zpět. Nepokoušejte se pokračovat s jinými tvrzeními po chybě:

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
}

Pokud dávka nebo uložená procedura běží s SET XACT_ABORT ON, považujte jakoukoli chybu příkazu za ukončující transakci. Okamžitě proveďte rollback a nezkoušejte provádět další příkazy ani Commit(). Aktuální verze ovladačů detekují transakce zrušené serverem a vrátí chybu, místo aby umožnily tiché částečné potvrzení.

Savepoints

Savepointy vytvářejí mezitím vrácené body v rámci transakce. SQL Server nativně podporuje savepointy. Protože balíček Go database/sql nezobrazuje uložené body přímo, spusťte je jako surový SQL prostřednictvím transakce:

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> vytvoří bod obnovení. ROLLBACK TRANSACTION <name> Vrátí se zpět k uloženému bodu, aniž by ukončil vnější transakci. ROLLBACK Bez jména se celá transakce vrátí zpět.

Řešení uváznutí

SQL Server řeší zablokování tak, že ukončí jednu z konkurenčních transakcí (oběť zablokování) a vrátí chybu 1205. Ukončená transakce je automaticky vrácena serverem.

Detekce uváznutí a opakování pokusu

Zkontrolujte chybu 1205 a zkuste celou transakci znovu s krátkým zpožděním:

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

Použijte obal na opětovné zkusení mrtvého záseku

Předání transakční funkce do obalu pro opakované pokusy:

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

Omezte uváznutí

Strategy Jak to pomáhá
Přistupujte k tabulkám ve stejném pořadí Když všechny transakce uzamknou Tabulku A před Tabulkou B, nemůže dojít k kruhovému čekání.
Udržujte transakce krátké Kratší transakce drží zámky kratší dobu, což zmenšuje prostor pro konflikty.
Použijte minimální úroveň izolace ReadCommitted drží méně zámků než Serializable.
Přidejte vhodné indexy Aktualizace zaměřené na indexy uzamknou méně řádků než skenování tabulek.
Vyhněte se interakci s uživatelem během transakcí Nikdy nečekejte na uživatelský vstup mezi BeginTx a .Commit

Opakování je správná odpověď v aplikačním kódu, ale opakované zablokování na stejném dotazu naznačuje problém v návrhu. Pomocí grafu vzájemného zablokování v SQL Serveru (zachyceného prostřednictvím Extended Events nebo relace system_health) identifikujte konkurenční příkazy a typy zámků a poté použijte strategie uvedené v předchozí tabulce. Úplný návod k analýze a prevenci uváznutí viz v průvodci uváznutími.

Transakce a připnutí spojení

Transakce připíná jedno spojení z poolu, dokud Commit()Rollback() není vyvoláno nebo je vyvoláno. Během této doby žádná jiná gorutina nemůže toto spojení využít.

Důsledky:

  • Dlouhodobé transakce snižují efektivní velikost fondu. Pokud máte v MaxOpenConns=25 otevřeno 20 transakcí, pro ostatní úlohy je k dispozici pouze 5 připojení.
  • Zapomenuté Rollback() způsobí trvalý únik spojení.
  • Zrušení kontextu transakce vrátí transakci zpět a vrátí spojení do poolu.
// 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()

Současné transakce

Každá gorutina by měla vytvořit vlastní transakci. Nikdy nesdílejte *sql.Tx mezi gorutinami, protože *sql.Tx není bezpečný pro souběžné použití:

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

Distribuované transakce

Ovladač go-mssqldb nepodporuje distribuované transakce (XA transakce nebo System.Transactions jejich ekvivalenty). Pokud potřebujete koordinovat práci napříč více databázemi:

  • Použijte ságový vzor s kompenzačními akcemi.
  • Pokud je to možné, konsolidujte operace do jedné databáze.
  • Použijte propojené servery s BEGIN DISTRIBUTED TRANSACTION z jazyka Transact-SQL (T-SQL), pokud jsou obě databáze SQL Server.

Kontrolní seznam transakcí

Area Recommendation
Ochrana proti zpětnému ústupu Vždy defer tx.Rollback() hned po BeginTx.
Úroveň izolace Začněte s ReadCommitted (výchozí). Eskalujte jen tehdy, když je to nutné.
Deadlocks Zabalte transakční kód do opakovací smyčky. Přistupujte ke tabulkám v konzistentním pořadí.
Doba trvání Udržujte transakce co nejkratší. Nastavte časové limity kontextu.
Concurrency Nikdy nesdílejte *sql.Tx mezi gorutinami.
Savepoints Použijte SAVE TRANSACTION a ROLLBACK TRANSACTION <name> pro částečné vrácení zpět.
Dopad na bazén Monitorujte db.Stats().InUse, abyste odhalili úniky připojení způsobené nepotvrzenými transakcemi.