死锁在交易中重试

时间:2011-05-13 18:11:36

标签: c# transactions deadlock

我有C#窗口服务,它与MS SQL服务器上的多个数据库通信。它是多线程的,并且具有许多功能,每个功能都有很长的数据库操作列表,每个功能都在它们自己的事务下运行。所以典型的功能就像

    public void DoSomeDBWork()
    {
        using (TransactionScope ts = new TransactionScope(TransactionScopeOption.RequiresNew))
        {
            DatabaseUpdate1();
            DatabaseUpdate2();
            DatabaseUpdate3();
            DatabaseUpdate4();
            DatabaseUpdate5();

            DatabaseUpdate6();

        }
    }

在重载下,我们遇到了死锁。我的问题是,如果我编写一些C#代码以便在发生死锁时自动重新提交DatabaseUpdate,它是否会阻止未提交的操作的资源?例如,如果在DatabaseUpdate6()中发生死锁异常并且我在等待3秒后重试3次,则在此期间所有未提交的操作“DatabaseUpdates 1到5”都会保留其资源,这可能会进一步增加更多僵局?在遇到死锁的情况下重试是否是一个好习惯。

4 个答案:

答案 0 :(得分:10)

你正在咆哮错误的树。

死锁意味着撤消整个交易范围。根据您的应用程序,您可以从using块重新启动,即。一个新的TransactionScope,但这非常不可能是正确的。您看到死锁的原因是其他人已经更改了您正在更改的数据。由于大多数更新都将更新应用于先前从数据库中读取的值,因此死锁清楚地表明您读取的内容已更改。因此,无需再次阅读即可应用您的更新将覆盖其他事务所更改的内容,从而导致更新丢失。这就是为什么死锁几乎永远不会被“自动”重试,新数据必须从数据库重新加载,如果涉及用户操作(例如表单编辑),则必须通知用户并且必须重新验证更改,然后才能再次尝试更新。只有某种类型的自动处理操作可以退出,但它们永远不会像“尝试再写”一样重试,但它们总是在“读取 - 更新 - 写入”循环中执行,而死锁会导致循环再次尝试,因为他们总是从'读'开始。它们会自动进行自我纠正。

话虽如此,您的代码很可能因为在不需要时滥用序列化隔离级别而导致死锁:using new TransactionScope() Considered Harmful。您必须覆盖事务选项以使用ReadCommitted隔离级别,几乎不需要序列化,这是实现死锁的有保证的方法。

第二个问题是为什么序列化死锁?由于表扫描导致死锁,这表明您没有适当的索引来进行读取和更新。

最后一个问题是您使用RequiresNew,99%的案例,这也是错误的。除非您真正深入了解正在发生的事情以及需要独立事务的防弹案例,否则您应始终使用Required并在呼叫者的包含事务中登记。

答案 1 :(得分:5)

这不包括您问题中的所有内容,而是涉及重试的主题。重试交易,数据库与否的想法是危险的,你不应该读这个,如果“幂等”这个词对你来说没什么意义(坦率地说,我对它也不了解,但我的管理层已经说完了最后的话,关闭了我我写了一篇关于死锁的重试。我和几个我认识的最聪明的人在这个区域交谈过,他们都回来给我带来了“糟糕”,所以我觉得这个来源并不好。抛开免责声明,这样做也可以让它变得有趣...,这里我最近写的是在投掷和返回之前重试MySql死锁指定的次数

使用匿名方法,您只需要一个可以动态处理方法签名和泛型返回类型的接收器。你还需要一个类似的void返回,只需要使用Action()对于MSSQL,我认为它看起来几乎相同,减去'my'

  1. 执行重试的处理程序:

    //

    private T AttemptActionReturnObject<T>(Func<T> action)
            {
                var attemptCount = 0;
    
                do
                {
                    attemptCount++;
                    try
                    {
                        return action();
                    }
                    catch (MySqlException ex)
                    {
                        if (attemptCount <= DB_DEADLOCK_RETRY_COUNT)
                        {
                            switch (ex.Number)
                            {
                                case 1205: //(ER_LOCK_WAIT_TIMEOUT) Lock wait timeout exceeded
                                case 1213: //(ER_LOCK_DEADLOCK) Deadlock found when trying to get lock
                                    Thread.Sleep(attemptCount*1000);
                                    break;
                                default:
                                    throw;
                            }
                        }
                        else
                        {
                            throw;
                        }
                    }
                } while (true);
            }
    
  2. 使用delegate或lambda

    包装方法调用
        public int ExecuteNonQuery(MySqlConnection connection, string commandText, params MySqlParameter[] commandParameters)
    {
        try
        {
            return AttemptActionReturnObject( () => MySqlHelper.ExecuteNonQuery(connection, commandText, commandParameters) );
        }
        catch (Exception ex)
        {
            throw new Exception(ex.ToString() + " For SQL Statement:" + commandText);
        }
    }
    
  3. 它也可能如下所示:

    return AttemptActionReturnObject(delegate { return MySqlHelper.ExecuteNonQuery(connection, commandText, commandParameters); });
    

答案 2 :(得分:1)

我认为不同的数据库服务器可能会以不同的方式响应死锁,如果两个事务处于死锁状态,则会使用SQL Server,而服务器将其选为死锁牺牲品(错误1205)并回滚该事务。这当然意味着另一个交易能够继续进行。

如果您是死锁受害者,则必须重做所有数据库更新,而不仅仅是update6。

在回应有关使用NOLOCK等提示避免死锁的评论时,我强烈反对。

僵局只是生活中的一个事实。想象一下,两个用户各自向会计系统提交手工日记帐分录 第一个条目是银行账户和信用卡的信用额度。应收账款的借方。 第二个条目是ar&amp;的借方。信用银行。

现在想象两个交易同时发生(在测试中很少发生的事情)

交易1锁定银行账户 交易2锁定a / r账户 事务1试图锁定应收账款并阻止等待交易2。 事务2尝试锁定银行并自动立即检测到死锁。 其中一个事务被选为死锁的受害者并被回滚。另一笔交易就好像什么都没发生一样。

僵局是现实,回应它们的方式非常直接。 “请挂断电话再试一次。”

有关使用SQL Server处理死锁的详细信息,请参阅MSDN

答案 3 :(得分:1)

当SQL检测到死锁时,它会终止一个线程并报告错误。如果您的线程被终止,它会自动回滚所有未提交的事务 - 在您的情况下,在最近的事务中已经运行的所有DatabaseUpdate*()

解决这个问题的方法完全取决于您的环境。如果你有类似控制表或字符串表的东西,它没有更新,但经常阅读。您可以使用NOLOCK ...提示踢和尖叫......当您不担心时间或交易敏感信息时,它实际上非常有用。但是,当您处理易失性或有状态信息时,您无法使用NOLOCK,因为它会导致意外行为。

有两种方法可以处理我使用的死锁。当您检测到故障时,直接从头开始重新启动事务。或者您可以在使用它们之前读取变量,然后执行。第二种是资源占用,并且性能显着下降,因此不应该用于高容量功能。