Ответ
Использование нескольких независимых транзакций в одной функции — это, как правило, анти-паттерн, поскольку это нарушает атомарность всей операции.
В чем опасность?
Рассмотрим пример с двумя последовательными транзакциями:
func ProcessData(ctx context.Context, db *sql.DB) error {
tx1, err := db.BeginTx(ctx, nil)
if err != nil { return err }
defer tx1.Rollback() // Rollback сработает, только если Commit не был вызван
// ... какие-то операции в tx1 ...
if err := tx1.Commit(); err != nil { return err }
// !!! ОПАСНЫЙ МОМЕНТ !!!
// Если следующая транзакция завершится с ошибкой,
// первая транзакция (tx1) уже закоммичена и не будет отменена.
// Это приведет к несогласованности данных.
tx2, err := db.BeginTx(ctx, nil)
if err != nil { return err }
defer tx2.Rollback()
// ... какие-то операции в tx2 ...
return tx2.Commit()
}
Роль context.Context
Контекст (ctx
) в данном случае служит для отмены и установки дедлайнов. Он будет корректно распространен на обе транзакции и все внутренние запросы (ExecContext
, QueryContext
). Если контекст будет отменен (например, из-за таймаута HTTP-запроса), то любая операция, использующая этот контекст, вернет ошибку, что позволит прервать выполнение. Однако контекст не решает проблему атомарности.
Правильные подходы
-
Одна большая транзакция (предпочтительный способ) Все операции, которые должны быть атомарны, следует выполнять в рамках одной транзакции.
func ProcessDataAtomically(ctx context.Context, db *sql.DB) error { tx, err := db.BeginTx(ctx, nil) if err != nil { return err } defer tx.Rollback() // Гарантирует откат в случае любой ошибки // Операция 1 // ... tx.ExecContext(ctx, ...) ... // Операция 2 // ... tx.ExecContext(ctx, ...) ... return tx.Commit() // Все изменения применяются только здесь }
-
Паттерн "Сага" (для распределенных систем) Если операции должны выполняться в разных сервисах или базах данных, используются более сложные паттерны, такие как "Сага", где для каждой операции предусматривается компенсирующая операция отката.