go-mssqldb ile işlemler

İşlemler, birden fazla işlemi atomik bir birimde gruplar. Ya tüm işlemler başarıyla tamamlanır (commit edilir) ya da hiçbirinin etkisi olmaz (rollback yapılır). Bu makale, sürücüyle go-mssqldb yapılan işlemlerin nasıl kullanılacağını, izolasyon seviyeleri, hata yönetimi ve üretim uygulamaları için kalıplar dahil olmak üzere ele alır.

Bu makaledeki örnekler AdventureWorks2025 örnek veritabanıyla karşılaştırılıyor. Yazma odaklı örnekler HumanResources.Department, Production.ProductCategory, Production.ProductSubcategory ve Production.ProductInventory öğelerini hedefler.

Bir işlem başlatın

Bir işlem başlatmak için db.BeginTx kullanın. Geri dönen *sql.Tx cihaz, işlem süresi boyunca havuzdan tek bir bağlantıyı pinler:

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

Her zaman defer tx.Rollback() işleminden hemen sonra BeginTx çağırın. Commit() başarılı olursa, ertelenmiş Rollback() işlem yapmaz. Commit() öncesinde herhangi bir hata oluşursa, ertelenmiş Rollback(), işlemin açık kalmamasını ve bağlantının onaylanmamış durumda havuza geri dönmemesini sağlar.

Yalıtım düzeyleri

SQL Server, eşzamanlı işlemlerin nasıl etkileşime girdiğini kontrol eden birkaç izolasyon seviyesini destekler. İzolasyon seviyesini şu sql.TxOptionsşekilde ayarlayın:

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

İzolasyon seviyesi karşılaştırması

Yalıtım düzeyi Kirli okumalar Tekrar Edilemez Okumalar Hayalet okumaları Performans etkisi Şu durumlarda kullanın:
sql.LevelReadUncommitted Evet Evet Evet En düşük genel gider Yaklaşık sayılar, izleme gösterge panelleri. Veri doğruluğu kritik değil.
sql.LevelReadCommitted Hayır Evet Evet Varsayılan. Çoğu iş yükü için iyi. Genel OLTP iş yükleri. Varsayılan ve önerilen başlangıç noktası.
sql.LevelRepeatableRead Hayır Hayır Evet Orta. Kilitleri daha uzun tutar. İşlem boyunca aynı satırlar için tutarlı değerler görmesi gereken okuma işlemleri.
sql.LevelSerializable Hayır Hayır Hayır En yüksek. Menzil kilitleri eşzamanlı girişleri engeller. Finansal işlemler, envanter yönetimi, hayalet okuma her yer kabul edilemez.
sql.LevelSnapshot Hayır Hayır Hayır Sıralı versiyonlama kullanır tempdb. Engelleme yok. Okuma ağırlıklı iş yükleri, yazarları engellemeden zaman içinde tutarlılık gerektiriyor.

Note

sql.LevelSnapshot veritabanında anlık görüntü izolasyonunun etkinleştirilmesi gerekir: ALTER DATABASE AdventureWorks2025 SET ALLOW_SNAPSHOT_ISOLATION ON.

Örnek: Read committed ve serileştirilebilir

İzolasyon seviyesini belirtin 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,
})

Salt okunur yönlendirme

Sürücü, sql.TxOptions.ReadOnly öğesini desteklemiyor. ReadOnly: true iletilirse, BeginTx bir hata döndürür.

AlwaysOn salt okunur yönlendirmesi için, bağlantıyı açtığınızda bağlantı dizesinde applicationintent=ReadOnly ayarını yapın:

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

Bu bağlantı seviyesindeki bir ayardır. Uygun oturumları okunabilir bir ikincile yönlendirebilir, ancak mevcut bir işlemi salt okunur hâle getirmez. Veri yazmaması gereken iş yükleri için özel bir yalnızca okunabilir bağlantı veya en az ayrıcalıklı kimlik bilgileri kullanın.

İşlemlerde hata yönetimi

İşlemlerdeki hataları dikkatlice ele alın. Herhangi bir komut başarısız olursa, işlem tamamen geri alınmalıdır. Bir hata sonrası başka ifadelerle devam etmeye çalışmayın:

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
}

Eğer toplu veya depolanan prosedür ile SET XACT_ABORT ONçalışıyorsa, herhangi bir ifade hatasını işlem için terminal olarak kabul eder. Hemen geri alın ve başka komutlar veya Commit() denemeyin. Mevcut sürücü sürümleri, sunucu tarafından sonlandırılan işlemleri algılar ve işlemin bir kısmının sessizce onaylanmasına izin vermek yerine hata döndürür.

Kayıt noktaları

Kayıt puanları, bir işlem içinde ara geri dönüş noktaları oluşturur. SQL Server, kayıt noktalarını doğal olarak destekliyor. Go'nun paketi database/sql kayıt noktalarını doğrudan açığa çıkarmadığından, bunları işlem üzerinden ham SQL olarak çalıştırın:

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> kaydetme noktasını oluşturur. ROLLBACK TRANSACTION <name> dış işlemi sonlandırmadan o geri alma noktasına geri döner. ROLLBACK isim olmadan tüm işlem geri alınıyor.

Kilitlenme işleme

SQL Server, rakip işlemlerden birini (çıkmaz kurbanı) sonlandırarak ve 1205 hatasını döndürerek çıkmazları çözer. Sonlandırılan işlem sunucu tarafından otomatik olarak geri alınır.

Kilitlenmeleri algıla ve yeniden dene

1205 hatasını kontrol edin ve tüm işlemi kısa bir gecikmeyle tekrar deneyin:

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

Deadlock yeniden deneme ambalajını kullanın

Bir işlem fonksiyonunu yeniden deneme wrapper'ına geçirin:

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

Kilitlenmeleri azalt

Strategy Nasıl yardımcı olur?
Tablolara tutarlı bir sırada erişin Tüm işlemler Tablo A'yı Tablo B'den önce kilitlediğinde, dairesel beklemeler gerçekleşemez.
İşlemleri kısa tutun Daha kısa işlemler kilitleri daha kısa süre tutar ve çatışma penceresini azaltır.
En düşük yeterli izolasyon seviyesini kullanın ReadCommitteddaha az kilit tutar.Serializable
Uygun indeksler ekleyin Endeks hedefli güncellemeler, tablo taramalarından daha az satır kilitler.
İşlemler sırasında kullanıcı etkileşiminden kaçının BeginTx ile Commit arasında kullanıcı girdisi beklemeyin.

Uygulama kodunda tekrar denemek doğru yanıttır, ancak aynı sorguda tekrar tekrar çıkmak bir tasarım sorunu olduğunu gösterir. SQL Server kilitlenme grafiğini (Genişletilmiş Olaylar veya sistem durumu oturumu aracılığıyla yakalanan) kullanarak çakışan deyimleri ve kilit türlerini belirleyin, ardından önceki tablodaki stratejileri uygulayın. Deadlock analizi ve önleme konularında tam bir rehber için Deadlocks rehberine bakınız.

İşlemler ve bağlantı sabitleme

Bir işlem, Commit() veya Rollback() çağrılana kadar havuzdan tek bir bağlantıyı ayırır. Bu süre zarfında başka hiçbir goroutine bu bağlantıyı kullanamaz.

Sonuçlar:

  • Uzun süreli işlemler etkili havuz boyutunu azaltır. Eğer 20 açık işleminiz varsa MaxOpenConns=25 , diğer işler için sadece 5 bağlantı mümkündür.
  • Unutulan Rollback(), bağlantının kalıcı olarak sızmasına neden olur.
  • İşlemin bağlamındaki bağlam iptali, işlemi geri alır ve bağlantıyı havuza geri getirir.
// 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()

Eşzamanlı işlemler

Her goroutine kendi transaksiyonunu oluşturmalıdır. *sql.Tx eşzamanlı kullanım için güvenli olmadığından, bir *sql.Tx öğesini asla goroutine’ler arasında paylaşmayın:

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

Dağıtılmış işlemler

Sürücü, go-mssqldb dağıtık işlemleri (XA işlemleri veya System.Transactions eşdeğerleri) desteklemez. Birden fazla veritabanı arasında çalışmayı koordine etmeniz gerekiyorsa:

  • Telafi edici eylemlerle bir saga deseni kullanın.
  • Operasyonları mümkün olduğunda tek bir veritabanında birleştirin.
  • Her iki veritabanı da SQL Server ise, Transact-SQL (T-SQL) içinden BEGIN DISTRIBUTED TRANSACTION ile bağlantılı sunucuları kullanın.

İşlem kontrol listesi

Area Recommendation
Geri dönme güvenliği Her zaman defer tx.Rollback() hemen sonra BeginTx.
Yalıtım düzeyi (Varsayılan olan) ile ReadCommitted başlayın. Sadece gerektiğinde yükseltme.
Deadlocks İşlemsel kodu bir yeniden deneme döngüsü içine alın. Tablolara tutarlı bir sırayla erişin.
Süre İşlemleri mümkün olduğunca kısa tutun. Bağlam için son tarihler belirleyin.
Concurrency *sql.Tx öğesini goroutine'ler arasında asla paylaşmayın.
Kayıt noktaları Kısmi geri alma için SAVE TRANSACTION ve ROLLBACK TRANSACTION <name> kullanın.
Havuz etkisi Onaylanmamış işlemlerden kaynaklanan bağlantı sızıntılarını tespit etmek için db.Stats().InUse öğesini izleyin.