显式调用事务回滚还是让异常触发隐式回滚,哪种做法更好?

22

如果在执行SQL语句时发生任何异常,由于事务未提交,我们应该期望事务会被隐式回滚,因为它已经超出作用域并被处理:

using (DbTransaction tran = conn.BeginTransaction())
{
    //
    // Execute SQL statements here...
    //
    tran.Commit();
}

以上做法是否可行?还是应该捕获异常并显式调用tran.Rollback(),如下所示:

using (DbTransaction tran = conn.BeginTransaction())
{
    try
    {
        //
        // Execute SQL statements here...
        //
        tran.Commit();
    }
    catch
    {
        tran.Rollback();
        throw;
    }
}

1
哪一个最能传达意图? - Mitch Wheat
3个回答

24

以前的做法是,在类似于TransactionScope等主题上查找 MSDN 的示例,它们都支持隐式回滚。有很多原因,但我只能给你一个非常简单的原因:当你捕获异常时,事务可能已经回滚了。许多错误会回滚挂起的事务,然后将控制权返回到客户端,这时 ADO.Net 会在事务已经在服务器上回滚之后引发 CLR SqlException 异常(例如1205 DEADLOCK就是这种错误的典型案例),因此显式的Rollback()调用,最好情况下是空操作,最糟情况下是一种错误。提供DbTransaction(例如SqlTransaction)的提供者应该知道如何处理这种情况,例如,因为服务端和客户端之间有明确的交流,告知事务已经回滚,并且Dispose()方法会执行正确的操作。

第二个原因是事务可以嵌套,但ROLLBACK的语义是一个回滚会回滚所有事务,所以你只需要调用一次(与Commit()不同,它只提交内部最深层的事务,并且必须为每个事务调用配对)。同样,Dispose()会执行正确的操作。

更新

SqlConnection.BeginTransaction() 的 MSDN 示例实际上更喜欢第二种形式,在catch块中显式地使用了Rollback()。我怀疑技术作家只是想在一个单一的示例中显示Rollback()Commit(),请注意他需要在Rollback周围添加第二个try/catch块,以规避我最初提到的一些问题。


1
实际上,问题在于数据提供程序可能不会按照这种方式实现:“Dispose 应该回滚事务。但是,Dispose 的行为是特定于提供程序的,不应替代调用 Rollback。”来自 MSDN - Andre Luus
话虽如此,我更喜欢隐式方法。 - Andre Luus
@AndreLuus:好发现。请注意,您链接的MSDN仅适用于4.5版本,即在我写答案时,这还没有出现在MSDN上。 - Remus Rusanu
真的。现在我们至少有了微软的官方立场。 - Andre Luus
如果我必须使用 try-catchcatch{} 中进行一些错误日志记录,那么我是否应该显式调用 Rollback() - Michael
@RemusRusanu 感谢您在帖子中添加“更新”部分。像我这样的一些读者可能会感到困惑,为什么 MSDN 技术作家在他/她的 MSDN 示例中使用显式的 Rollback - nam

3
您可以根据个人喜好选择两种不同的方式,前一种更简洁,后一种更能表达意图。
第一种方法需要注意的是,在事务处理结束时调用RollBack可能会受到驱动程序特定实现的限制。希望几乎所有的.NET连接器都能够做到这点,例如SqlTransaction
private void Dispose(bool disposing)
{
    Bid.PoolerTrace("<sc.SqlInteralTransaction.Dispose|RES|CPOOL> %d#, Disposing\n", this.ObjectID);
    if (disposing && (this._innerConnection != null))
    {
        this._disposing = true;
        this.Rollback();
    }
}

MySQL:

protected override void Dispose(bool disposing)
{
  if ((conn != null && conn.State == ConnectionState.Open || conn.SoftClosed) && open)
    Rollback();
  base.Dispose(disposing);
}

第二种方法需要注意的是,如果没有另一个try-catch块保护,调用RollBack是不安全的。 文档中明确说明了这一点。

简而言之,哪种方法更好取决于驱动程序,但通常最好选择第一种方法,原因如Remus所述。

此外,请参阅未提交事务在连接关闭时会发生什么?,了解连接处理提交和回滚的情况。


2

我倾向于支持基于异常路径的“隐式”回滚。但是,显然这取决于您在堆栈中的位置以及您要完成的任务(即DBTranscation类是否捕获异常并执行清理操作,还是被动地不“提交”)。

这里有一个情况,隐式处理可能是有意义的:

static T WithTranaction<T>(this SqlConnection con, Func<T> do) {
    using (var txn = con.BeginTransaction()) {
        return do();
    }
}

但是,如果API不同,则提交处理也可能不同(当然这个:

static T WithTranaction<T>(this SqlConnection con, Func<T> do, 
    Action<SqlTransaction> success = null, Action<SqlTransaction> failure = null) 
{
    using (var txn = con.BeginTransaction()) {
        try {
            T t = do();
            success(txn); // does it matter if the callback commits?
            return t;
        } catch (Exception e) {
            failure(txn); // does it matter if the callback rolls-back or commits?
            // throw new Exception("Doh!", e); // kills the transaction for certain
            // return default(T); // if not throwing, we need to do something (bogus)
        }
    }
}

除了需要强制执行改变控制策略的情况外,我想不出太多需要显式回滚的情况。但是,我有点反应慢。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接