Примечание.
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Транзакции объединяют несколько операций в атомарную единицу. Либо все операции успешны (фиксация), либо ни одна из них не вступает в силу (откат). В этой статье рассматривается, как использовать транзакции с драйвером go-mssqldb , включая уровни изоляции, обработку ошибок и шаблоны для производственных приложений.
Примеры в этой статье выполняются в образце базы данных AdventureWorks2025. Примеры для операций записи ориентированы на HumanResources.Department, Production.ProductCategory, Production.ProductSubcategory и Production.ProductInventory.
Начать транзакцию
Используйте db.BeginTx для начала сделки.
*sql.Tx закрепляет одно соединение из пула на всё время выполнения транзакции:
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
Всегда звоните defer tx.Rollback() сразу после BeginTx. Если выполнение Commit() прошло успешно, то отложенный Rollback() не выполняет никаких действий. Если какая-либо ошибка возникает до Commit(), отложенный Rollback() гарантирует, что транзакция не останется открытой и что соединение не будет возвращено в пул в неподтверждённом состоянии.
Уровни изоляции
SQL Server поддерживает несколько уровней изоляции, контролирующих взаимодействие одновременных транзакций. Установить уровень изоляции в sql.TxOptions:
tx, err := db.BeginTx(ctx, &sql.TxOptions{
Isolation: sql.LevelReadCommitted,
})
Сравнение уровней изоляции
| Уровень изоляции | Грязные считывания | Неповторимые чтения | Фантомное чтение | Влияние на производительность | Используйте, если |
|---|---|---|---|---|---|
sql.LevelReadUncommitted |
Yes | Yes | Yes | Минимальные накладные расходы | Приблизительный подсчёт, мониторинговые панели. Точность данных не критична. |
sql.LevelReadCommitted |
No | Yes | Yes | По умолчанию. Подходит для большинства рабочих нагрузок. | Общие нагрузки по OLTP. Стандартная и рекомендуемая отправная точка. |
sql.LevelRepeatableRead |
No | No | Yes | Умеренный. Держит замки дольше. | Чтения, которые должны видеть согласованные значения для тех же строк внутри транзакции. |
sql.LevelSerializable |
No | No | No | Наивысший. Блокировки диапазона блокируют параллельные вставки. | Финансовые операции, управление запасами, любые фантомные считывания — это недопустимо. |
sql.LevelSnapshot |
No | No | No | Использует версионирование строк в tempdb. Никаких блокировок. |
Нагрузка с большим количеством чтения, требующая стабильности в момент времени, не блокируя авторов. |
Note
sql.LevelSnapshot требует включения выделения снимков в базе данных: ALTER DATABASE AdventureWorks2025 SET ALLOW_SNAPSHOT_ISOLATION ON.
Пример: Read committed vs. Serializable
Укажите уровень изоляции в 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,
})
Маршрутизация только для чтения
Драйвер не поддерживает sql.TxOptions.ReadOnly. Если вы передаёте ReadOnly: true, BeginTx возвращает ошибку.
Для маршрутизации подключений только для чтения AlwaysOn установите applicationintent=ReadOnly в строке подключения при открытии подключения:
sqlserver://listener.example.com?database=AdventureWorks2025&applicationintent=ReadOnly
Это настройка на уровне соединения. Он может направлять подходящие сеансы на вторичный сервер, доступный для чтения, но не переводит существующую транзакцию в режим только чтения. Используйте выделенное подключение только для чтения или учетные данные с наименьшими привилегиями для рабочих нагрузок, которые не должны записывать данные.
Обработка ошибок в транзакциях
Аккуратно обрабатывайте ошибки в транзакциях. Если какая-либо выписка не проходит, транзакция должна быть полностью откатена. Не пытайтесь продолжать другие утверждения после ошибки:
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
}
Если пакетная или сохранённая процедура выполняется с SET XACT_ABORT ON, считайте любую ошибку оператора терминальной для транзакции. Немедленно откатитесь назад и не пытайтесь делать новые заявления или Commit(). Текущие версии драйверов обнаруживают транзакции, прерванные сервером, и возвращают ошибку вместо того, чтобы допускать тихое частичное подтверждение транзакции.
Точки сохранения
Точки сохранения создают промежуточные точки отката внутри транзакции. SQL Server поддерживает точки сохранения нативно. Поскольку пакет Go database/sql не предоставляет прямого доступа к точкам сохранения, выполняйте их как необработанные SQL-команды через транзакцию:
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> Создаёт точку сохранения.
ROLLBACK TRANSACTION <name> откатывается обратно к этой точке сохранения, не завершая внешнюю транзакцию.
ROLLBACK без имени откатывает всю транзакцию.
Обработка тупика
SQL Server устраняет тупиковые блокировки, завершая одну из конкурирующих транзакций (жертву тупика) и возвращая ошибку 1205. Завершившаяся транзакция автоматически откатывается сервером.
Обнаружение и повторная проверка тупиков
Проверьте ошибку 1205 и повторите всю транзакцию с небольшой задержкой:
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
Передайте функцию транзакции в обёртку для повторных попыток:
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
})
Уменьшить тупиковые блокировки
| Strategy | Как это помогает |
|---|---|
| Обращаться к таблицам в одном и том же порядке | Когда все транзакции блокируют таблицу A перед таблицей B, круговые ожидания не могут происходить. |
| Держите транзакции короткими | Короткие транзакции удерживают блокировки в течение меньшего времени, что сокращает окно для конфликтов. |
| Используйте минимальный уровень изоляции |
ReadCommitted содержит меньше замков, чем Serializable. |
| Добавьте соответствующие индексы | Обновления, ориентированные на индекс, блокируют меньше строк, чем сканы таблиц. |
| Избегайте взаимодействия пользователей во время транзакций | Никогда не ждите ввода пользователя между BeginTx и Commit. |
Повторная попытка — это правильный ответ в коде приложения, но повторяющиеся блокировки по одному и тому же запросу указывают на проблему проектирования. Используйте граф взаимоблокировки SQL Server, полученный с помощью Extended Events или сеанса system_health, чтобы определить конфликтующие инструкции и типы блокировок, а затем примените стратегии из предыдущей таблицы. Для полного обзора анализа и предотвращения тупиков смотрите руководство по Deadlocks.
Транзакции и закрепление соединений
Транзакция закрепляет за собой одно соединение из пула до вызова Commit() или Rollback(). В это время ни одна другая горутина не может использовать это соединение.
Последствия:
- Долгосрочные сделки уменьшают эффективный размер пула. Если у вас есть
MaxOpenConns=25и 20 открытых транзакций, для других задач доступно только 5 подключений. - Из-за забытого
Rollback()соединение навсегда остаётся незакрытым. - Отмена контекста в контексте транзакции откатывает транзакцию назад и возвращает соединение в пул.
// 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()
Параллельные транзакции
Каждая горутина должна создавать собственную транзакцию. Никогда не передавайте *sql.Tx между горутинами, потому что *sql.Tx небезопасен для одновременного использования:
// 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()
Распределенные транзакции
Драйвер go-mssqldb не поддерживает распределённые транзакции (транзакции XA или System.Transactions их аналоги). Если вам нужно координировать работу между несколькими базами данных:
- Используйте паттерн саги с компенсирующими действиями.
- Консолидировать операции в единую базу данных, когда это возможно.
- Используйте связанные серверы с
BEGIN DISTRIBUTED TRANSACTIONиз Transact-SQL (T-SQL), если обе базы данных размещены в SQL Server.
Чек-лист транзакций
| Area | Recommendation |
|---|---|
| Безопасность при откате | Всегда defer tx.Rollback() сразу после BeginTx. |
| Уровень изоляции | Начните с ReadCommitted (по умолчанию). Эскалируйте только при необходимости. |
| Взаимоблокировки | Оберните транзакционный код в цикл повторных попыток. Обращайтесь к таблицам в одном и том же порядке. |
| Duration | Держите транзакции как можно короче. Устанавливайте контекстные сроки. |
| Concurrency | Никогда не используйте *sql.Tx совместно между горутинами. |
| Точки сохранения | Используйте SAVE TRANSACTION и ROLLBACK TRANSACTION <name> для частичного отката. |
| Влияние на бассейн | Отслеживайте db.Stats().InUse, чтобы выявлять утечки соединений, вызванные неподтверждёнными транзакциями. |