Hinweis
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, sich anzumelden oder das Verzeichnis zu wechseln.
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, das Verzeichnis zu wechseln.
Transaktionen gruppieren mehrere Operationen zu einer atomaren Einheit. Entweder sind alle Operationen erfolgreich (commit) oder keine davon tritt in Kraft (Rollback). Dieser Artikel behandelt, wie man Transaktionen mit dem go-mssqldb Treiber verwendet, einschließlich Isolationsstufen, Fehlerbehandlung und Mustern für Produktionsanwendungen.
Beispiele in diesem Artikel laufen gegen die AdventureWorks2025-Beispieldatenbank . Schreiborientierte Beispiele richten sich an HumanResources.Department, Production.ProductCategory, Production.ProductSubcategory und Production.ProductInventory.
Starte eine Transaktion
Nutze db.BeginTx es, um eine Transaktion zu starten. Die zurückgegebenen *sql.Tx Pins verbinden eine einzelne Verbindung aus dem Pool für die Lebensdauer der Transaktion:
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
Rufen Sie defer tx.Rollback() immer unmittelbar nach BeginTx an. Wenn Commit() gelingt, ist das aufgeschobene Rollback() ein no-op. Tritt zuvor Commit()ein Fehler auf, sorgt das Deferred Rollback() dafür, dass die Transaktion nicht offen bleibt und die Verbindung in einem ungebundenen Zustand zurück zum Pool leckt.
Isolationsebenen
SQL Server unterstützt mehrere Isolationsstufen, die steuern, wie gleichzeitige Transaktionen miteinander interagieren. Setzen Sie das Isolationsniveau in sql.TxOptions:
tx, err := db.BeginTx(ctx, &sql.TxOptions{
Isolation: sql.LevelReadCommitted,
})
Vergleich der Isolationsebene
| Isolationsstufe | Unsichere Lesevorgänge | Nicht wiederholbare Lesevorgänge | Phantomlesevorgänge | Auswirkungen auf die Leistung | Verwenden Sie, wenn |
|---|---|---|---|---|---|
sql.LevelReadUncommitted |
Ja | Ja | Ja | Geringster Aufwand | Ungefähre Zählungen, Überwachungs-Dashboards. Die Datengenauigkeit ist nicht entscheidend. |
sql.LevelReadCommitted |
No | Ja | Ja | Standard. Gut für die meisten Arbeitslasten. | Allgemeine OLTP-Workloads. Der Standard- und empfohlene Ausgangspunkt. |
sql.LevelRepeatableRead |
No | No | Ja | Mäßig. Hält Sperren länger. | Lesevorgänge, die innerhalb der Transaktion für dieselben Zeilen konsistente Werte lesen müssen. |
sql.LevelSerializable |
No | No | No | Maximal. Bereichssperren blockieren gleichzeitige Einfügungen. | Finanztransaktionen, Bestandsverwaltung – überall dort, wo Phantom-Reads inakzeptabel sind. |
sql.LevelSnapshot |
No | No | No | Verwendet Versionsverwaltung auf Zeilenebene in tempdb. Kein Blocken. |
Leseintensive Arbeitslasten, die zeitliche Konsistenz erfordern, ohne Autoren zu blockieren. |
Note
sql.LevelSnapshot erfordert die Aktivierung der Snapshot-Isolation in der Datenbank: ALTER DATABASE AdventureWorks2025 SET ALLOW_SNAPSHOT_ISOLATION ON.
Beispiel: Read committed vs. serialisierbar
Spezifizieren Sie das Isolationsniveau in 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,
})
Schreibgeschütztes Routing
Der Treiber unterstützt sql.TxOptions.ReadOnlynicht . Wenn du ReadOnly: true übergibst, gibt BeginTx einen Fehler zurück.
Für AlwaysOn-Routing für schreibgeschützte Verbindungen legen Sie beim Öffnen der Verbindung applicationintent=ReadOnly in der Verbindungszeichenfolge fest:
sqlserver://listener.example.com?database=AdventureWorks2025&applicationintent=ReadOnly
Dies ist eine Einstellung auf Verbindungsebene. Es kann berechtigte Sitzungen an eine lesbare sekundäre Sitzung routen, aber eine bestehende Transaktion wird nicht nur lesbar. Verwenden Sie für Workloads, die keine Daten schreiben dürfen, eine dedizierte schreibgeschützte Verbindung oder Zugangsdaten mit den geringstmöglichen Berechtigungen.
Fehlerbehandlung bei Transaktionen
Gehen Sie mit Vorsicht mit Fehlern in Transaktionen um. Wenn eine Aussage fehlschlägt, muss die Transaktion vollständig zurückgesetzt werden. Versuchen Sie nicht, nach einem Fehler mit anderen Aussagen fortzufahren:
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
}
Wenn die Batch- oder gespeicherte Prozedur mit ausgeführt wird SET XACT_ABORT ON, behandeln Sie jeden Anweisungsfehler als Endpunkt für die Transaktion. Zieh sofort zurück und versuche keine weiteren Aussagen oder Commit(). Aktuelle Treiberversionen erkennen vom Server abgebrochene Transaktionen und melden einen Fehler, anstatt einen unbemerkten teilweisen Commit zuzulassen.
Sicherungspunkte
Speicherpunkte erzeugen zwischenliegende Rollback-Punkte innerhalb einer Transaktion. SQL Server unterstützt Speicherpunkte nativ. Da Gos database/sql Paket Speicherpunkte nicht direkt offenlegt, führen Sie sie als rohes SQL über die Transaktion aus:
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> erstellt den Speicherpunkt.
ROLLBACK TRANSACTION <name> rollt zu diesem Speicherpunkt zurück, ohne die äußere Transaktion zu beenden.
ROLLBACK ohne Angabe eines Namens macht die gesamte Transaktion rückgängig.
Deadlock-Behandlung
SQL Server löst Deadlocks, indem eine der konkurrierenden Transaktionen (das Opfer des Deadlocks) beendet und Fehler 1205 zurückgegeben wird. Die beendete Transaktion wird automatisch vom Server zurückgesetzt.
Deadlocks erkennen und erneut versuchen
Überprüfen Sie den Fehler 1205 und versuchen Sie die gesamte Transaktion mit kurzer Verzögerung erneut:
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)
}
Verwenden Sie den Deadlock-Retry-Wrapper
Geben Sie eine Transaktionsfunktion an den Retry-Wrapper weiter:
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
})
Blockaden verringern
| Strategy | Wie es hilft |
|---|---|
| Zugriffstabellen in konsistenter Reihenfolge | Wenn alle Transaktionen Tabelle A vor Tabelle B sperren, können keine zirkulären Wartezeiten stattfinden. |
| Halten Sie die Transaktionen kurz | Kürzere Transaktionen halten die Sperren kürzer und verkürzen so das Zeitfenster für Konflikte. |
| Verwenden Sie das niedrigste ausreichende Isolationsniveau |
ReadCommitted hält weniger Schlösser als Serializable. |
| Fügen Sie passende Indizes hinzu | Indexbasierte Updates sperren weniger Zeilen als Tabellenscans. |
| Nutzerinteraktion während Transaktionen vermeiden | Warte niemals auf Benutzereingaben zwischen BeginTx und Commit. |
Ein erneuter Versuch ist die korrekte Antwort im Anwendungscode, aber wiederholte Deadlocks bei derselben Abfrage deuten auf ein Designproblem hin. Verwenden Sie den SQL Server Deadlock-Graphen (erfasst durch Extended Events oder die Systemgesundheitssitzung), um die konkurrierenden Anweisungen und Sperrtypen zu identifizieren, und wenden Sie dann die Strategien in der vorherigen Tabelle an. Eine vollständige Anleitung zur Analyse und Vermeidung von Deadlocks finden Sie im Deadlocks-Leitfaden.
Transaktionen und Bindung an eine Verbindung
Eine Transaktion pinnt eine einzelne Verbindung aus dem Pool, bis Commit() oder Rollback() aufgerufen wird. Während dieser Zeit kann keine andere Goroutine diese Verbindung nutzen.
Implikationen:
- Langlaufende Transaktionen verringern die effektive Poolgröße. Wenn Sie
MaxOpenConns=25und 20 offene Transaktionen haben, stehen nur 5 Verbindungen für andere Aufgaben zur Verfügung. - Ein vergessenes
Rollback()verursacht dauerhaft ein Verbindungsleck. - Das Abbrechen des Kontexts der Transaktion setzt die Transaktion zurück und gibt die Verbindung an den Pool zurück.
// 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()
Gleichzeitige Transaktionen
Jede GoRoutine sollte ihre eigene Transaktion erstellen. Geben Sie ein *sql.Tx niemals zwischen Goroutines weiter, da *sql.Tx nicht sicher gleichzeitig verwendet werden kann:
// 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()
Verteilte Transaktionen
Der Treiber go-mssqldb unterstützt keine verteilten Transaktionen (XA-Transaktionen oder System.Transactions Äquivalente). Wenn Sie die Arbeit über mehrere Datenbanken hinweg koordinieren müssen:
- Verwende ein Saga-Muster mit ausgleichenden Aktionen.
- Konsolidieren Sie die Operationen, wenn möglich, in einer einzigen Datenbank.
- Verwenden Sie verknüpfte Server mit
BEGIN DISTRIBUTED TRANSACTIONaus Transact-SQL (T-SQL), wenn beide Datenbanken SQL Server sind.
Transaktionscheckliste
| Area | Recommendation |
|---|---|
| Rückrollsicherung | Immer defer tx.Rollback() unmittelbar nach BeginTx. |
| Isolationsstufe | Beginne mit ReadCommitted (dem Standard). Eskaliere nur, wenn es nötig ist. |
| Deadlocks | Wickle transaktionalen Code in eine Retry-Schleife. Greifen Sie in einer einheitlichen Reihenfolge auf Tabellen zu. |
| Dauer | Halten Sie die Transaktionen so kurz wie möglich. Setzen Sie Kontextfristen. |
| Konkurrenz | Teile niemals eine *sql.Tx zwischen Goroutinen. |
| Sicherungspunkte | Verwenden Sie SAVE TRANSACTION und ROLLBACK TRANSACTION <name> für ein teilweises Rollback. |
| Auswirkung des Schwimmbeckens | Überwachen db.Stats().InUse Sie, um Verbindungslecks durch nicht gebundene Transaktionen zu erkennen. |