Catatan
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba masuk atau mengubah direktori.
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba mengubah direktori.
Transaksi mengelompokkan beberapa operasi ke dalam satu unit atom. Semua operasi berhasil (commit) atau tidak satu pun diterapkan (rollback). Artikel ini membahas cara menggunakan transaksi dengan go-mssqldb driver, termasuk tingkat isolasi, penanganan kesalahan, dan pola untuk aplikasi produksi.
Contoh dalam artikel ini menggunakan basis data sampel AdventureWorks2025. Contoh yang berorientasi pada penulisan menargetkan HumanResources.Department, Production.ProductCategory, Production.ProductSubcategory, dan Production.ProductInventory.
Memulai transaksi
Gunakan db.BeginTx untuk memulai transaksi.
*sql.Tx yang dikembalikan mengunci satu koneksi dari pool selama transaksi berlangsung:
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
Selalu hubungi defer tx.Rollback() segera setelah BeginTx. Jika Commit() berhasil, yang ditangguhkan Rollback() adalah no-op. Jika ada kesalahan yang terjadi sebelumnya Commit(), yang ditangguhkan Rollback() memastikan transaksi tidak tetap terbuka dan membocorkan koneksi kembali ke kumpulan dalam status tidak dilakukan.
Tingkat isolasi
SQL Server mendukung beberapa tingkat isolasi yang mengontrol bagaimana transaksi bersamaan berinteraksi. Atur tingkat isolasi di:sql.TxOptions
tx, err := db.BeginTx(ctx, &sql.TxOptions{
Isolation: sql.LevelReadCommitted,
})
Perbandingan tingkat isolasi
| Tingkat isolasi | Bacaan kotor | Pembacaan yang tidak dapat diulang | Pembacaan fantom | Dampak performa | Gunakan ketika diperlukan |
|---|---|---|---|---|---|
sql.LevelReadUncommitted |
Yes | Yes | Yes | overhead paling rendah | Perkiraan hitungan, dasbor pemantauan. Akurasi data tidak penting. |
sql.LevelReadCommitted |
No | Yes | Yes | Default. Bagus untuk sebagian besar beban kerja. | Beban kerja OLTP umum. Titik awal default dan yang direkomendasikan. |
sql.LevelRepeatableRead |
No | No | Yes | Moderat. Menahan kunci lebih lama. | Operasi baca harus menghasilkan nilai yang konsisten pada baris yang sama dalam transaksi. |
sql.LevelSerializable |
No | No | No | Tertinggi. Kunci rentang memblokir sisipan bersamaan. | Transaksi keuangan, manajemen inventaris, di mana pun phantom reads tidak dapat diterima. |
sql.LevelSnapshot |
No | No | No | Menggunakan pembuatan versi baris di tempdb. Tidak ada pemblokiran. |
Beban kerja yang didominasi operasi baca dan memerlukan konsistensi pada satu titik waktu tertentu tanpa menghambat operasi tulis. |
Note
sql.LevelSnapshot Memerlukan isolasi rekam jepret untuk diaktifkan pada database: ALTER DATABASE AdventureWorks2025 SET ALLOW_SNAPSHOT_ISOLATION ON.
Contoh: Baca yang dikomitkan vs. dapat diserialkan
Tentukan tingkat isolasi di 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,
})
Perutean baca-saja
Driver tidak mendukung sql.TxOptions.ReadOnly. Jika Anda lulus ReadOnly: true, BeginTx mengembalikan kesalahan.
Untuk perutean baca-saja AlwaysOn, atur applicationintent=ReadOnly dalam string koneksi saat Anda membuka koneksi:
sqlserver://listener.example.com?database=AdventureWorks2025&applicationintent=ReadOnly
Ini adalah pengaturan tingkat koneksi. Ini dapat mengarahkan sesi yang memenuhi syarat ke node sekunder yang dapat dibaca, tetapi tidak menjadikan transaksi yang sudah ada bersifat hanya-baca. Gunakan koneksi baca-saja khusus atau kredensial hak istimewa paling rendah untuk beban kerja yang tidak boleh menulis data.
Penanganan kesalahan dalam transaksi
Tangani kesalahan di dalam transaksi dengan hati-hati. Ketika ada pernyataan yang gagal, transaksi harus dibatalkan sepenuhnya. Jangan mencoba melanjutkan dengan pernyataan lain setelah kesalahan:
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
}
Jika batch atau prosedur tersimpan berjalan dengan SET XACT_ABORT ON, perlakukan kesalahan pernyataan apa pun sebagai terminal untuk transaksi. Segera mundur dan jangan mencoba lebih banyak pernyataan atau Commit(). Rilis driver terbaru mendeteksi transaksi yang dibatalkan oleh server dan mengembalikan error alih-alih membiarkan commit parsial terjadi tanpa pemberitahuan.
Titik simpan
Titik simpan membuat titik pengembalian perantara di dalam transaksi. SQL Server mendukung savepoint secara asli. Karena paket database/sql milik Go tidak menyediakan savepoint secara langsung, eksekusi savepoint tersebut sebagai SQL mentah melalui transaksi:
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> membuat titik penyimpanan.
ROLLBACK TRANSACTION <name> melakukan rollback ke savepoint tersebut tanpa mengakhiri transaksi terluar.
ROLLBACK yang tidak memiliki nama membatalkan seluruh transaksi.
Penanganan kebuntuan
SQL Server menyelesaikan kebuntuan dengan mengakhiri salah satu transaksi yang bersaing (korban kebuntuan) dan mengembalikan kesalahan 1205. Transaksi yang dihentikan dikembalikan secara otomatis oleh server.
Mendeteksi dan mencoba kembali kebuntuan
Periksa kesalahan 1205 dan coba kembali seluruh transaksi dengan penundaan singkat:
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)
}
Menggunakan pembungkus percobaan ulang kebuntuan
Teruskan fungsi transaksi ke wrapper percobaan ulang:
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
})
Mengurangi kebuntuan
| Strategy | Bagaimana hal itu membantu |
|---|---|
| Mengakses tabel dalam urutan yang konsisten | Ketika semua transaksi mengunci Tabel A sebelum Tabel B, kondisi saling tunggu melingkar tidak dapat terjadi. |
| Jaga agar transaksi tetap singkat | Transaksi yang lebih singkat mempertahankan penguncian dalam waktu yang lebih singkat, sehingga mengurangi kemungkinan terjadinya konflik. |
| Gunakan tingkat isolasi terendah yang cukup |
ReadCommitted memegang lebih sedikit kunci daripada Serializable. |
| Menambahkan indeks yang sesuai | Pembaruan yang menargetkan indeks mengunci lebih sedikit baris daripada pemindaian tabel. |
| Hindari interaksi pengguna selama transaksi | Jangan pernah menunggu input pengguna antara BeginTx dan Commit. |
Mencoba ulang adalah respons yang benar dalam kode aplikasi, tetapi kebuntuan berulang pada kueri yang sama menunjukkan masalah desain. Gunakan grafik deadlock SQL Server (yang ditangkap melalui Extended Events atau sesi system health) untuk mengidentifikasi pernyataan yang saling bersaing dan jenis kuncinya, lalu terapkan strategi dalam tabel sebelumnya. Untuk panduan lengkap tentang analisis dan pencegahan kebuntuan, lihat panduan tentang kebuntuan.
Transaksi dan penyematan koneksi
Transaksi menyematkan satu koneksi dari kumpulan hingga Commit() atau Rollback() dipanggil. Selama periode ini, tidak ada goroutine lain yang dapat menggunakan koneksi tersebut.
Implikasi:
- Transaksi yang berjalan lama mengurangi ukuran kumpulan efektif. Jika Anda memiliki
MaxOpenConns=25dan 20 transaksi terbuka, hanya 5 koneksi yang tersedia untuk pekerjaan lain. -
Rollback()yang terlupakan menyebabkan kebocoran koneksi secara permanen. - Pembatalan konteks pada konteks transaksi mengembalikan transaksi dan mengembalikan koneksi ke kumpulan.
// 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()
Transaksi bersamaan
Setiap goroutine harus membuat transaksinya sendiri. Jangan pernah membagikan *sql.Tx antar-goroutine karena *sql.Tx tidak aman digunakan secara bersamaan:
// 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()
Transaksi terdistribusi
go-mssqldb driver tidak mendukung transaksi terdistribusi (transaksi XA atau yang setara dengan System.Transactions). Jika Anda perlu mengoordinasikan pekerjaan di beberapa database:
- Gunakan pola saga dengan tindakan kompensasi.
- Konsolidasikan operasi ke dalam satu database jika memungkinkan.
- Gunakan server tertaut dengan
BEGIN DISTRIBUTED TRANSACTIONdari Transact-SQL (T-SQL) jika kedua database adalah SQL Server.
Daftar periksa transaksi
| Area | Recommendation |
|---|---|
| Keamanan pengembalian | Selalu defer tx.Rollback() segera setelah BeginTx. |
| Tingkat isolasi | Mulailah dengan ReadCommitted (default). Lakukan eskalasi hanya jika diperlukan. |
| Kebuntuan | Tempatkan kode transaksi dalam loop percobaan ulang. Akses tabel dalam urutan yang konsisten. |
| Durasi | Jaga agar transaksi sesingkat mungkin. Tetapkan batas waktu konteks. |
| Concurrency | Jangan pernah membagikan *sql.Tx di antara goroutine. |
| Titik simpan | Gunakan SAVE TRANSACTION dan ROLLBACK TRANSACTION <name> untuk pengembalian parsial. |
| Dampak kolam renang | Pantau db.Stats().InUse untuk mendeteksi kebocoran koneksi dari transaksi yang tidak dilakukan. |