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

Ответ

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

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

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

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

Ответ 18+ 🔞

Да ты посмотри на эту дичь, блядь! Сидит какой-нибудь юный гоблин и думает: "А че, а давайте я тут наворочу две транзакции подряд, будет охуенно!"

Ну слушай сюда, пизда с ушами. Использовать несколько независимых транзакций в одной функции — это ж классический анти-паттерн, ёпта! Это всё равно что построить мост из двух половинок, которые друг с другом не связаны. Одна уже стоит, а вторую только начали — и бац, пиздец, кораблик тонет.

В чём, блядь, подвох?

Смотри, вот тебе пример, от которого волосы дыбом встают:

func ProcessData(ctx context.Context, db *sql.DB) error {
    tx1, err := db.BeginTx(ctx, nil)
    if err != nil { return err }
    defer tx1.Rollback() // Откатится, только если не закоммитили

    // ... делаем что-то важное в tx1 ...
    if err := tx1.Commit(); err != nil { return err }

    // !!! А ВОТ ТУТ-ТО И НАЧИНАЕТСЯ ЦИРК, БЛЯДЬ !!!
    // Если вторая транзакция ебанётся с ошибкой,
    // первая-то уже улетела в базу навсегда! Её не откатить!
    // И сидишь ты с наполовину обновлёнными данными, как мудак.

    tx2, err := db.BeginTx(ctx, nil)
    if err != nil { return err }
    defer tx2.Rollback()

    // ... делаем что-то ещё в tx2 ...
    return tx2.Commit()
}

Вот и получается — первую операцию сделал, вторую просрал, а данные уже в говно. Красота!

А контекст-то нахуя?

Контекст (ctx) тут — это как надзиратель с секундомером. Он нужен для отмены и дедлайнов. Отменили запрос пользователя — контекст сдох, и все операции внутри транзакций, которые на него завязаны, тоже посыпятся с ошибкой. Удобно! Но, сука, он ни хуя не решает проблему атомарности. Он может только сказать "стоп, играем в ящик", но не склеит две отдельные транзакции в одну неделимую хуйню.

Так как делать-то, ебать?

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

    func ProcessDataAtomically(ctx context.Context, db *sql.DB) error {
        tx, err := db.BeginTx(ctx, nil)
        if err != nil {
            return err
        }
        defer tx.Rollback() // Страховочная верёвка. Ошибка? Автоматом в пизду всё.
    
        // Делаем первое
        // ... tx.ExecContext(ctx, ...) ...
    
        // Делаем второе
        // ... tx.ExecContext(ctx, ...) ...
    
        return tx.Commit() // И только тут, если всё чики-пуки, применяем разом.
    }
  2. Паттерн "Сага" (для сложных, распределённых пиздюлей) Если твои операции размазаны по разным сервисам или базам — одной транзакцией не обойдёшься. Тут нужна "Сага". Это когда для каждого действия есть своя "компенсирующая" операция отката. Сложно, муторно, но зато надёжно. Хотя, блядь, это уже высший пилотаж, не для новичков.

Короче, запомни: несколько транзакций подряд в одном месте — это верный признак, что ты где-то накосячил в логике. Думай головой, а не жопой!