Как управлять несколькими транзакциями в одной функции и как с ними работает контекст?

Ответ

Использование нескольких независимых транзакций в одной функции — это, как правило, анти-паттерн, поскольку это нарушает атомарность всей операции.

В чем опасность?

Рассмотрим пример с двумя последовательными транзакциями:

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-запроса), то любая операция, использующая этот контекст, вернет ошибку, что позволит прервать выполнение. Однако контекст не решает проблему атомарности.

Правильные подходы

  1. Одна большая транзакция (предпочтительный способ) Все операции, которые должны быть атомарны, следует выполнять в рамках одной транзакции.

    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() // Все изменения применяются только здесь
    }
  2. Паттерн "Сага" (для распределенных систем) Если операции должны выполняться в разных сервисах или базах данных, используются более сложные паттерны, такие как "Сага", где для каждой операции предусматривается компенсирующая операция отката.