Go: Commit失败后是否需要Rollback

栏目: IT技术 · 发布时间: 6年前

内容简介:最近使用如果起初我以为是 sqlmock 的 bug,但是追踪代码时发现,sqlmock 的 assertion 功能是正常的,问题出在

最近使用 sqlmock 编写单元测试时遇到一个问题。现有这样的代码:

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

如果 tx.Commit() 失败了,那么 Rollback 的 mock assertion 不会被触发。但跟踪代码时我看到 tx.Rollback() 路径确实会被执行到的。

起初我以为是 sqlmock 的 bug,但是追踪代码时发现,sqlmock 的 assertion 功能是正常的,问题出在 tx.Rollback() 确实没有调用 sqlmock 提供的 mock driver 的 Rollback 方法。

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 也会报错,当然一般人也不会这么写代码)

这里隐含一个对 driver 背后的数据库的要求:如果 Commit 失败了,需要保证一定会 Rollback。具体可以拆成三点:

  1. 一旦 Commit 失败,需要对当前事务自动 Rollback
  2. 如果客户端失去响应,比如连接断了,需要 Rollback 当前的事务
  3. 只有在收到客户端确认 Commit 结果的响应后,才算是 Commit 结束。如果 Commit 成功,但是客户端没有确认,那么还是得 Rollback 当前事务。

(也许读者会觉得第 3 点是第 2 点下的一个特例,不过我觉得第 3 点要求数据库在设计协议的时候需要有个 Commit Response 类型的消息,所以值得拎出来单独分一类)

至于 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。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。

数据结构与算法分析

数据结构与算法分析

(美)(C.A.谢弗)Clifford A.Shaffer / 电子工业出版社 / 1998-8 / 35.00元

本书综合“数据结构与算法”的知识梳理、习题解答及上机辅导等于一身;精心挑选了覆盖教学大纲的五百多道题目,并且提供所有题目的参考答案;对于较难的算法和上机题,给出了详细的分析和说明;对于学习的重点和难点、易犯的错误、题目的难易和重要性,以及国内教材的差异等都给出了必要的说明。 本书可给使用各种教材讲授和学习“数据结构与算法”(或者“数据结构”)的师生参考,是系统复习该课程和准备应考计算......一起来看看 《数据结构与算法分析》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换