事务中死锁重试

发布于 2024-11-06 18:23:33 字数 686 浏览 5 评论 0原文

我有 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”将保留其资源,这可能会进一步增加更多僵局?在出现死锁的情况下重试是否是一个好习惯?

I have C# window service which talks to multiple databases on a MS SQL server. It is multi threaded and has many functions each with long list of database operations, each of these functions run under their own transaction. So a typical function is like

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

            DatabaseUpdate6();

        }
    }

Under heavy load we are experiencing deadlocks. My question is, if I write some C# code to automatically resubmit the DatabaseUpdate in case of a deadlock will it hold back resources for uncommitted operations? for example , if a deadlock exception occurs in DatabaseUpdate6() and i retry it 3 times with a wait of 3 seconds, during this time will all the uncommitted operations "DatabaseUpdates 1 to 5" hold on to their resources which might further increase chances of more deadlocks ? Is it even a good practice to retry in case of deadlocks.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

半世蒼涼 2024-11-13 18:23:33

你找错了树。

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

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

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

最后一个问题是您使用 RequiresNew,这在 99% 的情况下都是不正确的。除非您对正在发生的事情有真正深入的了解,并且有充分的理由需要独立事务,否则您应该始终使用 Required 并加入调用者的包围事务。

You are barking up the wrong tree.

Deadlock means the entire transaction scope is undone. Depending on your application, you may be able to restart from the using block, ie. a new TransactionScope, but this is very very very unlikely to be correct. The reason you are seeing a deadlock is that someone else has changed data that you were changing too. Since most of these updated are applying the update to a value previously read from the database, the deadlock is a clear indication that whatever you've read was changed. So applying your updates w/o reading again is going to overwrite whatever was changed by the other transaction, thus causing lost updates. This is why deadlock can almost never be 'automatically' retried, the new data has to be reload from the db, if user action was involved (eg. form edit) then user has to be notified and has to re-validate the changes, and only then the update can be tried again. Only certain type of automatic processing actions can be retires, but they are never retried as in 'try to write again', but they always act in a loop of 'read-update-write' and deadlocks will cause the loop to try again, and since they always start with 'read'. They are automatically self-correcting.

That being said, your code deadlocks most likely because of abusing the serialization isolation level when not required: using new TransactionScope() Considered Harmful. You must overwrite the transaction options to use the ReadCommitted isolation level, serializable is almost never required and is a guaranteed way to achieve deadlocks.

Second issue is Why does serialization deadlock? It deadlocks because of table scans, which indicate you don't have proper indexes in place for your reads and your updates.

Last issue is that you use RequiresNew, which is again, 99% of the cases, incorrect. Unless you have real deep understanding of what's going on and a bulletproof case for requiring a standalone transaction, you should always use Required and enlist in the encompassing transaction of the caller.

反目相谮 2024-11-13 18:23:33

这并没有涵盖您问题中的所有内容,而是重试的主题。重试事务的想法,无论是否是数据库,都是危险的,如果“幂等”这个词对你来说毫无意义,你不应该阅读这篇文章(坦率地说,我对此也不太了解,但我的管理层有最终决定权,我我去写了关于死锁的重试,我和几个我在这个领域认识的最聪明的人交谈过,他们都给我回复了“糟糕糟糕”,所以我对提交该免责声明感到不舒服。这样做也可以让它变得有趣......,这是我最近写的东西,在抛出和返回之前重试 MySql 死锁指定的次数

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

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

    //

    private T AttemptActionReturnObject(Func 操作)
            {
                var 尝试计数 = 0;
    
                做
                {
                    尝试计数++;
                    尝试
                    {
                        返回动作();
                    }
                    捕获(MySqlException 前)
                    {
                        if (attemptCount <= DB_DEADLOCK_RETRY_COUNT)
                        {
                            开关(例如数字)
                            {
                                case 1205: //(ER_LOCK_WAIT_TIMEOUT) 超出锁等待超时
                                case 1213: //(ER_LOCK_DEADLOCK) 尝试获取锁时发现死锁
                                    Thread.Sleep(尝试计数*1000);
                                    休息;
                                默认:
                                    扔;
                            }
                        }
                        别的
                        {
                            扔;
                        }
                    }
                而(真);
            }
    
  2. 使用委托或 lambda 包装您的方法调用

     public int ExecuteNonQuery(MySqlConnection 连接, string commandText, params MySqlParameter[] commandParameters)
    {
        尝试
        {
            return AttemptActionReturnObject( () => MySqlHelper.ExecuteNonQuery(connection, commandText, commandParameters) );
        }
        catch(异常前)
        {
            抛出新的异常(例如ToString()+“对于SQL语句:”+commandText);
        }
    }
    

它也可能如下所示:

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

This doesn't cover everything in your question but on the subject of retries. The idea of retrying transactions, database or not, is dangerous and you should not read this if the word "idempotent" means nothing to you (frankly, i don't know enough about it either but my management had the final word and off I went to write in retries for deadlocks. I spoke to a couple of the smartest guys I know in this area and they all came back to me with "BAD BAD" so I don't feel good about committing that source. disclaimer aside, had to do it so may as well make it fun..., here's something I wrote recently to retry MySql deadlocks a specified number of times before throwing and returning

Using anonymous method you only have to have one receiver that can dynamically handle method signatures and generic return types. You'll also need a similar one for void return that will just need to use Action() For MSSQL it'll look pretty much identical I think, minus the 'my'

  1. The handler that does the retry:

    //

    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. Wrap your method call with delegate or 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);
        }
    }
    

it may also look like this:

return AttemptActionReturnObject(delegate { return MySqlHelper.ExecuteNonQuery(connection, commandText, commandParameters); });
千纸鹤带着心事 2024-11-13 18:23:33

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

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

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

When SQL detects a deadlock, it kills one thread and reports an error. If your thread is killed it automatically rolls back any uncommitted transactions - in your case ALL of the DatabaseUpdate*() that were already ran during this most recent transaction.

The ways to deal with this depend entirely on your environment. If you have something like a control table, or a string table, which is not updated, but frequently read. You can use NOLOCK... cue kicking and screaming... It is actually quite useful when you aren't worried about time or transaction sensitive information. However when you are dealing with volatile or stateful information you cannot use NOLOCK because it will lead to unexpected behavior.

There are two ways to handle deadlocks that I use. Either straight up restart the transaction from the beginning when you detect a failure. Or you can read in your variables before you use them, and execute afterwards. The second is something of a resource hog and sees significant decrease in performance so it should not be used for high-volume functionality.

你げ笑在眉眼 2024-11-13 18:23:33

我认为不同的数据库服务器对死锁的响应可能不同,但是对于 SQL Server,如果两个事务发生死锁,服务器将选择一个事务作为死锁受害者(错误 1205),并且该事务将回滚。这当然意味着其他交易能够继续进行。

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

为了回应有关使用 NOLOCK 等提示避免死锁的评论,我强烈建议不要使用它。

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

现在假设两个交易同时进行(这种情况在测试中很少发生)

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

僵局是一个现实,应对僵局的方法非常简单。 “请挂断电话并重试。”

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

I think different database servers may respond to a deadlock differently, howerver with SQL Server if two transactions are deadlocked one is elected by the server to as the deadlock victim (error 1205) and that transaction is rolled back. This means of course that the other transaction is able to proceed.

If you're the deadlock victim, you will have to redo all your database updates, not just update6.

In response to comments about avoiding deadlocks with hints such as NOLOCK, I would strongly recommand against it.

Deadlocks are simply a fact of life. Imagine, two users each submitting a manual journal entry into an accounting system
The first entry does a credit of the bank account & a debit of the receivables.
The second entry does a debit of the ar & credit bank.

Now imagine both transactions play at the same time (something that rarely if ever happens in testing)

transaction 1 locks the bank account
transaction 2 locks the a/r account.
transactions 1 tries to lock receivables and blocks waiting on transaction 2.
transaction 2 tries to lock the bank and a deadlock is automatically and instantly detected.
one of the transactions is elected as a victim of a deadlock and is rolled back. The other transaction proceeds as if nothing happened.

Deadlocks are a reality and the way to respond to them is quite straight forward. "please hang up and try your call again."

See MSDN for more information on Handling Deadlocks with SQL Server

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文