Poznámka:
Přístup k této stránce vyžaduje autorizaci. Můžete se zkusit přihlásit nebo změnit adresáře.
Přístup k této stránce vyžaduje autorizaci. Můžete zkusit změnit adresáře.
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=25otevř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 TRANSACTIONz 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. |