Notatka
Dostęp do tej strony wymaga autoryzacji. Może spróbować zalogować się lub zmienić katalogi.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
Transakcje grupują wiele operacji w jednostkę atomową. Albo wszystkie operacje zakończą się powodzeniem (zatwierdzenie), albo żadna z nich nie zostanie zastosowana (wycofanie). Ten artykuł omawia, jak korzystać z transakcji z sterownikiem go-mssqldb , w tym poziomy izolacji, obsługę błędów oraz wzorce dla zastosowań produkcyjnych.
Przykłady w tym artykule porównane są z przykładową bazą danych AdventureWorks2025 . Przykłady zorientowane na zapis obejmują HumanResources.Department, Production.ProductCategory, Production.ProductSubcategory i Production.ProductInventory.
Rozpocznij transakcję
Używaj db.BeginTx do rozpoczęcia transakcji. Zwrócone *sql.Tx piny stanowią pojedyncze połączenie z puli na cały okres trwania transakcji:
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()
Ważna
Zawsze wywołuj defer tx.Rollback() bezpośrednio po BeginTx. Jeśli Commit() zakończy się powodzeniem, odroczone Rollback() nie wykona żadnej operacji. Jeśli przed Commit() wystąpi jakikolwiek błąd, odroczone wywołanie Rollback() zapewnia, że transakcja nie pozostanie otwarta i że połączenie nie wróci do puli w stanie niezatwierdzonym.
Poziomy izolacji
SQL Server obsługuje kilka poziomów izolacji, które kontrolują, jak współbieżne transakcje współdziałają. Ustaw poziom izolacji w sql.TxOptions:
tx, err := db.BeginTx(ctx, &sql.TxOptions{
Isolation: sql.LevelReadCommitted,
})
Porównanie poziomów izolacji
| Poziom izolacji | Odczyty zanieczyszczone | Niepowtarzalne odczyty | Phantom czyta | Wpływ na wydajność | Użyj, gdy |
|---|---|---|---|---|---|
sql.LevelReadUncommitted |
Yes | Yes | Yes | Najniższe koszty ogólne | Przybliżone liczby, pulpity monitorowania. Dokładność danych nie jest kluczowa. |
sql.LevelReadCommitted |
No | Yes | Yes | Domyślne. Dobre do większości zadań. | Ogólne obciążenia OLTP. Domyślny i zalecany punkt wyjścia. |
sql.LevelRepeatableRead |
No | No | Yes | Umiarkowany. Dłużej utrzymuje zamki. | Odczyty, które muszą mieć spójne wartości dla tych samych wierszy w transakcji. |
sql.LevelSerializable |
No | No | No | Najwyższy. Blokady zakresowe blokują współbieżne operacje wstawiania. | Transakcje finansowe, zarządzanie zapasami, wszędzie tam, gdzie widmiczne odczyty są nie do przyjęcia. |
sql.LevelSnapshot |
No | No | No | Wykorzystuje wersjonowanie wierszy w tempdb. Bez blokowania. |
Obciążenia wymagające dużej ilości czytania, które wymagają spójności w określonym momencie bez blokowania autorów. |
Uwaga / Notatka
sql.LevelSnapshot wymaga włączenia izolacji migawek w bazie danych: ALTER DATABASE AdventureWorks2025 SET ALLOW_SNAPSHOT_ISOLATION ON.
Przykład: odczyt zatwierdzonych danych a poziom serializowalny
Określmy poziom izolacji w 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,
})
Rutowanie tylko odczytu
Sterownik nie obsługuje sql.TxOptions.ReadOnly. Jeśli przekażesz ReadOnly: true, BeginTx zwróci błąd.
W przypadku kierowania połączeń tylko do odczytu AlwaysOn ustaw applicationintent=ReadOnly w parametrach połączenia podczas otwierania połączenia:
sqlserver://listener.example.com?database=AdventureWorks2025&applicationintent=ReadOnly
To jest ustawienie na poziomie połączenia. Może kierować kwalifikowane sesje do czytelnego pliku wtórnego, ale nie sprawia, że istniejąca transakcja jest tylko do odczytu. Używaj dedykowanego połączenia w trybie tylko do odczytu lub poświadczeń z minimalnymi uprawnieniami dla obciążeń roboczych, które nie mogą zapisywać danych.
Obsługa błędów w transakcjach
Ostrożnie radzcie sobie z błędami w transakcjach. Jeśli jakakolwiek instrukcja zakończy się niepowodzeniem, transakcja musi zostać w całości wycofana. Nie próbuj kontynuować innych stwierdzeń po błędzie:
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
}
Jeśli partia lub procedura składowana jest uruchamiana z SET XACT_ABORT ON, każdy błąd w instrukcji należy traktować jako powodujący zakończenie transakcji. Cofnij się od razu i nie próbuj kolejnych instrukcji ani Commit(). Obecne wersje sterowników wykrywają przerwane transakcje serwera i zwracają błąd zamiast dopuszczać ciche częściowe zatwierdzenie.
Punkty zapisywania
Punkty przywracania tworzą pośrednie punkty wycofania w ramach transakcji. SQL Server natywnie obsługuje punkty zapisu. Ponieważ pakiet database/sql w Go nie udostępnia bezpośrednio punktów zapisu, wykonaj je jako surowe instrukcje SQL za pośrednictwem transakcji:
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()
}
Uwaga / Notatka
SAVE TRANSACTION <name> tworzy punkt zapisu.
ROLLBACK TRANSACTION <name> cofa się do tego punktu zapisu bez kończenia zewnętrznej transakcji.
ROLLBACK bez nazwy wycofuje całą transakcję.
Obsługa zakleszczeń
SQL Server rozwiązuje martwe punkty, kończąc jedną z konkurencyjnych transakcji (ofiarę zablokowania) i zwracając błąd 1205. Zakończona transakcja jest automatycznie cofana przez serwer.
Wykrywanie i ponowne próbowanie blokad
Sprawdź błąd 1205 i spróbuj ponownie przeprowadzić całą transakcję z krótkim opóźnieniem:
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)
}
Użyj mechanizmu ponawiania po zakleszczeniu
Przekaż funkcję transakcyjną do mechanizmu ponawiania:
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
})
Zmniejszenie blokad
| Strategy | Jak to pomaga |
|---|---|
| Uzyskuj dostęp do tabel w stałej kolejności | Gdy wszystkie transakcje blokują tabelę A przed tabelą B, nie może dojść do cyklicznego oczekiwania. |
| Utrzymuj transakcje krótkie | Krótsze transakcje utrzymują blokady przez krótszy czas, zmniejszając okres, w którym mogą wystąpić konflikty. |
| Użyj najniższego wystarczającego poziomu izolacji |
ReadCommitted Zawiera mniej zamków niż Serializable. |
| Dodaj odpowiednie indeksy | Aktualizacje ukierunkowane na indeks blokują mniej wierszy niż skanowanie tabel. |
| Unikaj interakcji użytkownika podczas transakcji | Nigdy nie czekaj na dane wejściowe od użytkownika między BeginTx a Commit. |
Ponowne próbowanie jest poprawną odpowiedzią w kodzie aplikacji, ale powtarzające się impaski na tym samym zapytaniu wskazują na problem projektowy. Użyj wykresu impasu SQL Server (zarejestrowanego za pomocą funkcji Extended Events lub sesji system_health), aby zidentyfikować kolidujące instrukcje i typy blokad, a następnie zastosować strategie z poprzedniej tabeli. Pełny przegląd analizy i zapobiegania impasom znajdziesz w przewodniku Deadlocks.
Transakcje i przypinanie połączeń
Transakcja rezerwuje pojedyncze połączenie z puli do momentu wywołania Commit() lub Rollback(). W tym czasie żadna inna gorutyna nie może korzystać z tego połączenia.
Implikacje:
- Transakcje długoterminowe zmniejszają efektywną wielkość puli. Jeśli masz
MaxOpenConns=25i 20 otwartych transakcji, dostępnych jest tylko 5 połączeń do innych zadań. - Zapomniane
Rollback()powoduje trwały wyciek połączenia. - Anulowanie kontekstu w kontekście transakcji cofa transakcję i przywraca połączenie do puli.
// 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()
Transakcje współistniejące
Każda goroutine powinna tworzyć własną transakcję. Nigdy nie udostępniaj elementu *sql.Tx pomiędzy gorutynami, ponieważ *sql.Tx nie jest bezpieczne do współbieżnego użycia:
// 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()
Transakcje rozproszone
Sterownik go-mssqldb nie obsługuje transakcji rozproszonych (transakcji XA lub System.Transactions ich odpowiedników). Jeśli musisz skoordynować pracę w wielu bazach danych:
- Użyj wzorca sagi z działaniami kompensacyjnymi.
- Konsoliduj operacje w jednej bazie danych, gdy to możliwe.
- Korzystaj z serwerów połączonych za pomocą
BEGIN DISTRIBUTED TRANSACTIONz języka Transact-SQL (T-SQL), jeśli obie bazy danych działają w programie SQL Server.
Lista kontrolna transakcji
| Area | Zalecenie |
|---|---|
| Bezpieczeństwo wycofywania zmian | Zawsze zaraz defer tx.Rollback() po BeginTx. |
| Poziom izolacji | Zacznij od ReadCommitted (domyślnie). Eskaluj tylko wtedy, gdy jest to konieczne. |
| Deadlocks | Opakuj kod transakcyjny w pętlę powtórek. Dostęp do tabel w spójnej kolejności. |
| Czas trwania | Utrzymuj transakcje tak krótkie, jak to możliwe. Ustaw limity czasu kontekstu. |
| Concurrency | Nigdy nie udostępniaj elementu *sql.Tx między goroutines. |
| Punkty zapisywania | Użyj SAVE TRANSACTION i ROLLBACK TRANSACTION <name> do częściowego wycofania. |
| Wpływ na basen | Monitoruj db.Stats().InUse, aby wykrywać wycieki połączeń wynikające z niezatwierdzonych transakcji. |