事务中的死锁重试

本文关键字:重试 死锁 事务 | 更新日期: 2023-09-27 17:56:36

我有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"是否会保留其资源,这可能会进一步增加更多死锁的机会? 在死锁的情况下重试甚至是一种好的做法。

事务中的死锁重试

你吠错了树。

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

话虽如此,您的代码死锁很可能是因为在不需要时滥用序列化隔离级别:使用新的 TransactionScope() 被认为是有害的。必须覆盖事务选项才能使用 ReadCommit 隔离级别,几乎从不需要可序列化,并且是实现死锁的保证方法。

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

最后一个问题是你使用 RequiresNew ,这又是 99% 的情况,不正确。除非您对正在发生的事情有真正深刻的理解,并且需要独立事务的无懈可击的情况,否则您应该始终使用 Required 并登记在调用方的包含事务中。

这并不涵盖您问题中的所有内容,而是关于重试的主题。 重试事务的想法,无论是否数据库,都是危险的,如果"幂等"这个词对你没有任何意义,你不应该阅读这个(坦率地说,我对此也不够了解,但我的管理层有最终决定权,然后我去写死锁的重试。 我和我认识的几个在这个领域最聪明的人谈过,他们都带着"BAD BAD"回到我身边,所以我对提交这个来源感觉不好。 撇开免责声明不谈,不得不这样做,这样也让它变得有趣......,这是我最近写的东西,在抛出和返回之前重试 MySql 死锁指定次数

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

  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. 使用委托或 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);
        }
    }
    

它也可能看起来像这样:

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

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

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

我使用的死锁有两种方法。检测到故障时,请直接从开头重新启动事务。或者,您可以在使用变量之前读取变量,然后在之后执行。第二个是资源消耗,性能显着下降,因此不应将其用于大批量功能。

我认为不同的数据库服务器可能会以不同的方式响应死锁,如果两个事务死锁,则使用 SQL Server,服务器选择其中一个作为死锁受害者(错误 1205),并且该事务被回滚。这当然意味着其他交易能够继续进行。

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

针对有关使用NOLOCK等提示避免死锁的评论,我强烈反对它。

僵局只是生活中的现实。 想象一下,两个用户各自向会计系统提交手动日记账分录第一个条目是银行账户的贷方和应收账款的借方。第二个条目对ar和信用银行进行借记。

现在想象一下两个事务同时播放(这在测试中很少发生)

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

僵局是现实,应对它们的方法非常简单。"请挂断电话,再试一次。"

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