事务范围过早完成

67

我有一段代码,在TransactionScope内运行,在这段代码中,我对数据库进行了多次调用。 包括选择、更新、创建和删除等操作。当我执行删除操作时,我使用SqlCommand的扩展方法执行它,如果此查询可能会出现死锁,它将自动重新提交查询。

我认为问题出在出现死锁时,函数尝试重新提交查询。 这就是我收到的错误消息:

与当前连接关联的事务已完成但未被处理。 必须处理该事务后才能使用连接来执行SQL语句。

这是执行查询的简单代码(以下所有代码都在TransactionScope的using内部执行):

using (sqlCommand.Connection = new SqlConnection(ConnectionStrings.App))
{
    sqlCommand.Connection.Open();
    sqlCommand.ExecuteNonQueryWithDeadlockHandling();
}

这里是一个扩展方法,可以重新提交死锁查询:

public static class SqlCommandExtender
{
    private const int DEADLOCK_ERROR = 1205;
    private const int MAXIMUM_DEADLOCK_RETRIES = 5;
    private const int SLEEP_INCREMENT = 100;

    public static void ExecuteNonQueryWithDeadlockHandling(this SqlCommand sqlCommand)
    {
        int count = 0;
        SqlException deadlockException = null;

        do
        {
            if (count > 0) Thread.Sleep(count * SLEEP_INCREMENT);
            deadlockException = ExecuteNonQuery(sqlCommand);
            count++;
        }
        while (deadlockException != null && count < MAXIMUM_DEADLOCK_RETRIES);

        if (deadlockException != null) throw deadlockException;
    }

    private static SqlException ExecuteNonQuery(SqlCommand sqlCommand)
    {
        try
        {
            sqlCommand.ExecuteNonQuery();
        }
        catch (SqlException exception)
        {
            if (exception.Number == DEADLOCK_ERROR) return exception;
            throw;
        }

        return null;
    }
}

错误发生在这一行:

sqlCommand.ExecuteNonQuery();
7个回答

63

不要忘记在TransactionScope中抑制你的select语句。即使你使用了with(nolock),在SQL Server 2005及以上版本中,锁仍然会被创建在那些被查询所涉及到的表上。查看此链接,它展示了如何设置和使用TransactionScope

using(TransactionScope ts = new TransactionScope 
{ 
  // db calls here are in the transaction 
  using(TransactionScope tsSuppressed = new TransactionScope (TransactionScopeOption.Suppress)) 
  { 
    // all db calls here are now not in the transaction 
  } 
} 

1
谢谢你提供的关于在选择语句中抑制事务的提示。这帮助我解决了一个让我疯狂的超时问题。 - RB Davidson
2
太棒了的答案。这在一组选择/插入SQL指令的集合上让我疯狂了。添加Suppress选项自动解决了问题。 - Jacob
1
天啊,谢谢你!我一整天都在苦苦挣扎。原来解决方案这么简单。 - rossipedia
2
过度热衷于锁定通常是默认的Serializable TS隔离级别的结果。通常最好的解决方法是将TS创建包装在一个类工厂中,该类工厂将序列化设置为更合理的选项,例如Read Comitted。详见:https://dev59.com/sGgu5IYBdhLWcg3we3Ct - StuartLC
1
我不明白。这与错误消息有什么关系?链接的文章是关于分布式事务的。为什么你想要压制任何东西 - 你锁定多少取决于隔离级别。 - John
我同意@John的观点,这个答案并没有真正解决问题。 - Johan Boulé

42
我发现当一个事务运行时间超过 System.TransactionsmaxTimeout 时,就会出现这个消息。即使增加了 TransactionOptions.Timeout 的值,也不能超过 maxTimeoutmaxTimeout 的默认值设置为10分钟,它的值只能在 machine.config 中进行修改。
请在配置级别下添加以下内容到 machine.config 中以修改超时时间:
<configuration>
    <system.transactions>
        <machineSettings maxTimeout="00:30:00" />
    </system.transactions>
</configuration>

机器配置文件可以在以下位置找到:%windir%\Microsoft.NET\Framework\[version]\config\machine.config

您可以阅读此博客文章了解更多信息:http://thecodesaysitall.blogspot.se/2012/04/long-running-systemtransactions.html


4
请注意,配置文件对大小写敏感,因此应该是"machineSettings"和"maxTimeout"。很遗憾你无法在app.config文件中覆盖这个设置 :( - Bram Vandenbussche
1
请注意,您必须将此放置在配置部分的末尾,否则会出现错误。 - Shaul Behr

23

我可以重现这个问题。这是一个事务超时。

using (new TransactionScope(TransactionScopeOption.Required, new TimeSpan(0, 0, 0, 1)))
{
    using (SqlConnection connection = new SqlConnection(connectionString))
    {
        connection.Open();
        using (var sqlCommand = connection.CreateCommand())
        {
            for (int i = 0; i < 10000; i++)
            {
                sqlCommand.CommandText = "select * from actor";
                using (var sqlDataReader = sqlCommand.ExecuteReader())
                {
                    while (sqlDataReader.Read())
                    {
                    }
                }
            }
        }
    }
}

抛出 System.InvalidOperationException 异常,并显示以下信息:

当前连接关联的事务已完成,但未被处理。在使用连接执行 SQL 语句之前,必须处理该事务。

要解决此问题,请使查询运行更快或增加超时时间。


12

如果在 TransactionScope 内部发生异常,事务会被回滚。这意味着 TransactionScope 已经结束了。现在你必须调用 dispose() 并开始一个新的事务。我不确定是否可以重复使用旧的 TransactionScope ,因为我从来没有试过,但我认为是不行的。


2
即使捕获了异常,事务也会回滚吗? - Hungry Beast
我从未尝试过这个,因为对我来说,异常=错误=停止和回滚。但是,从你的描述中看起来似乎不是这样。 - Donnie
2
这是错误的,也不是异常处理的情况。你也不必调用Dispose()。当TransactionScope在using语句中生成时,using语句将在异常情况下Dispose() TransactionScope。 - thewhiteambit
1
在Dispose()方法中,TransactionScope将根据是否调用了TransactionScope.Complete()来回滚或提交。这就是为什么在结束using块之前,Complete()必须作为最后一件事情被调用的原因。当然,您也可以使用try-finally块手动调用Dispose()。但是,这并不会改变关于Dispose()、异常和回滚行为的任何错误假设。抱歉,我不得不投反对票。 - thewhiteambit
我也遇到了同样的问题,但是能够理解答案。我正在这样使用:using (TransactionScope scope = new TransactionScope(TransactionScopeOption.RequiresNew)) - Ziggler

8

我的问题很愚蠢,如果你在调试中超时了却不退出,就会出现这种情况。 面对掌击

编程有时会让你感到很笨...


6

确认这个错误也可能是由于事务超时引起的。补充一下Marcus和Rolf所说的,如果你没有显式地设置TransactionScope的超时时间,那么超时时间间隔将会假定一个默认值。这个默认值是以下两者中较小的一个:

  1. If you've overridden the local app.config / web.config setting, e.g.

    <system.transactions>
    <defaultSettings timeout="00:05:00" />
    </system.transactions>
    
  2. But this is then 'capped' at the machine.config setting <machineSettings maxTimeout="00:10:00" />


我能在app.config文件中_覆盖_这个吗? - Kiquenet

1
这个异常也可能是由于禁用了Microsoft Distributed Transaction Coordinator所致。
如果我们想要启用它,我们运行"dcomcnfg"并选择"Component Services" -> "My Computer" -> "Distributed Transaction Coordinator" -> "Local Service DTC"并选择"属性"。
应该勾选"允许远程客户端"、"允许入站"、"允许出站"和"不需要身份验证"。

你能否通过 BAT 或 ps1 的编程方式来 启用/禁用 它? - Kiquenet

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