Kommentar
Åtkomst till den här sidan kräver auktorisering. Du kan prova att logga in eller ändra kataloger.
Åtkomst till den här sidan kräver auktorisering. Du kan prova att ändra kataloger.
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=25och 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 TRANSACTIONfrå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. |