Go: Commit失敗後是否須要Rollback

最近使用sqlmock編寫單元測試時遇到一個問題。現有這樣的代碼:git

defer func() {
  if err != nil {
    err = tx.Rollback()
    ...
  }
}
err = tx.Commit()

若是 tx.Commit() 失敗了,那麼 Rollback 的 mock assertion 不會被觸發。但跟蹤代碼時我看到 tx.Rollback() 路徑確實會被執行到的。github

起初我覺得是 sqlmock 的 bug,可是追蹤代碼時發現,sqlmock 的 assertion 功能是正常的,問題出在 tx.Rollback() 確實沒有調用 sqlmock 提供的 mock driver 的 Rollback 方法。sql

Go 的 Tx.Rollback() 方法實現以下:數據庫

// Rollback aborts the transaction.
func (tx *Tx) Rollback() error {
    return tx.rollback(false)
}

對應的 rollback 方法:函數

// rollback aborts the transaction and optionally forces the pool to discard
// the connection.
func (tx *Tx) rollback(discardConn bool) error {
    if !atomic.CompareAndSwapInt32(&tx.done, 0, 1) {
        return ErrTxDone
    }
    var err error
    withLock(tx.dc, func() {
        err = tx.txi.Rollback()
    })
    if err != driver.ErrBadConn {
        tx.closePrepared()
    }
    if discardConn {
        err = driver.ErrBadConn
    }
    tx.close(err)
    return err
}

若是 tx.done 已經被設置了,會直接返回 ErrTxDone 錯誤,不會調用 tx.txi.Rollback() 這個 callback。單元測試

// ErrTxDone is returned by any operation that is performed on a transaction
// that has already been committed or rolled back.
var ErrTxDone = errors.New("sql: transaction has already been committed or rolled back")

顯然 tx.done 是在 tx.Commit 的時候設置的。測試

func (tx *Tx) Commit() error {
    // Check context first to avoid transaction leak.
    // If put it behind tx.done CompareAndSwap statement, we can't ensure
    // the consistency between tx.done and the real COMMIT operation.
    select {
    default:
    case <-tx.ctx.Done():
        if atomic.LoadInt32(&tx.done) == 1 {
            return ErrTxDone
        }
        return tx.ctx.Err()
    }
    if !atomic.CompareAndSwapInt32(&tx.done, 0, 1) {
        return ErrTxDone
    }
    var err error
    withLock(tx.dc, func() {
        err = tx.txi.Commit()
    })
    if err != driver.ErrBadConn {
        tx.closePrepared()
    }
    tx.close(err)
    return err
}

看了以後確實如此,並且 tx.done 是在調用 driver 的 tx.txi.Commit() 以前設置。也就是說,不管 Commit 的結果是否成功,只要執行了,再執行 Rollback 必然會返回 ErrTxDone 錯誤。(一樣道理,若是已經 Rollback 了,再 Commit 也會報錯,固然通常人也不會這麼寫代碼)atom

這裏隱含一個對 driver 背後的數據庫的要求:若是 Commit 失敗了,須要保證必定會 Rollback。具體能夠拆成三點:設計

  1. 一旦 Commit 失敗,須要對當前事務自動 Rollback
  2. 若是客戶端失去響應,好比鏈接斷了,須要 Rollback 當前的事務
  3. 只有在收到客戶端確認 Commit 結果的響應後,纔算是 Commit 結束。若是 Commit 成功,可是客戶端沒有確認,那麼仍是得 Rollback 當前事務。

(也許讀者會以爲第 3 點是第 2 點下的一個特例,不過我以爲第 3 點要求數據庫在設計協議的時候須要有個 Commit Response 類型的消息,因此值得拎出來單獨分一類)code

至於 driver 的用戶,若是不能把 Commit 移到 defer 裏面(好比函數底部存在耗時操做,須要提早 Commit 掉),能夠把 Rollback 代碼改爲這樣,避免無謂的報錯:

defer func() {
  if err != nil {
    err = tx.Rollback()
    if err != sql.ErrTxDone {
      ...
    }
  }
}
err = tx.Commit()

或者:

defer func() {
  if err != nil {
    err = tx.Rollback()
    ...
  }
}
// 注意下面多了個冒號,這樣 Commit 返回的 err 和 defer 裏的 err
// 是兩個不一樣的錯誤,不會觸發 Rollback
err := tx.Commit()
// 隨便一提,若是 defer 的函數裏引用了 err,後面就不要用
// err := func() 的寫法,由於這種會覆蓋掉前面被引用的 err。
// x, err := func() 這種寫法是 OK 的,不會覆蓋掉 err。
相關文章
相關標籤/搜索