我有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”都会保留其资源,这可能会进一步增加更多僵局?在遇到死锁的情况下重试是否是一个好习惯。
答案 0 :(得分:10)
你正在咆哮错误的树。
死锁意味着撤消整个交易范围。根据您的应用程序,您可以从using
块重新启动,即。一个新的TransactionScope,但这非常不可能是正确的。您看到死锁的原因是其他人已经更改了您正在更改的数据。由于大多数更新都将更新应用于先前从数据库中读取的值,因此死锁清楚地表明您读取的内容已更改。因此,无需再次阅读即可应用您的更新将覆盖其他事务所更改的内容,从而导致更新丢失。这就是为什么死锁几乎永远不会被“自动”重试,新数据必须从数据库重新加载,如果涉及用户操作(例如表单编辑),则必须通知用户并且必须重新验证更改,然后才能再次尝试更新。只有某种类型的自动处理操作可以退出,但它们永远不会像“尝试再写”一样重试,但它们总是在“读取 - 更新 - 写入”循环中执行,而死锁会导致循环再次尝试,因为他们总是从'读'开始。它们会自动进行自我纠正。
话虽如此,您的代码很可能因为在不需要时滥用序列化隔离级别而导致死锁:using new TransactionScope() Considered Harmful。您必须覆盖事务选项以使用ReadCommitted隔离级别,几乎不需要序列化,这是实现死锁的有保证的方法。
第二个问题是为什么序列化死锁?由于表扫描导致死锁,这表明您没有适当的索引来进行读取和更新。
最后一个问题是您使用RequiresNew
,99%的案例,这也是错误的。除非您真正深入了解正在发生的事情以及需要独立事务的防弹案例,否则您应始终使用Required
并在呼叫者的包含事务中登记。
答案 1 :(得分:5)
这不包括您问题中的所有内容,而是涉及重试的主题。重试交易,数据库与否的想法是危险的,你不应该读这个,如果“幂等”这个词对你来说没什么意义(坦率地说,我对它也不了解,但我的管理层已经说完了最后的话,关闭了我我写了一篇关于死锁的重试。我和几个我认识的最聪明的人在这个区域交谈过,他们都回来给我带来了“糟糕”,所以我觉得这个来源并不好。抛开免责声明,这样做也可以让它变得有趣...,这里我最近写的是在投掷和返回之前重试MySql死锁指定的次数
使用匿名方法,您只需要一个可以动态处理方法签名和泛型返回类型的接收器。你还需要一个类似的void返回,只需要使用Action()对于MSSQL,我认为它看起来几乎相同,减去'my'
执行重试的处理程序:
//
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);
}
使用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);
}
}
它也可能如下所示:
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,因为它会导致意外行为。
有两种方法可以处理我使用的死锁。当您检测到故障时,直接从头开始重新启动事务。或者您可以在使用它们之前读取变量,然后执行。第二种是资源占用,并且性能显着下降,因此不应该用于高容量功能。