避免 SQL 事务中的死锁

Posted

技术标签:

【中文标题】避免 SQL 事务中的死锁【英文标题】:Avoiding Deadlocks within SQL transaction 【发布时间】:2016-08-29 12:13:06 【问题描述】:

我有以下代码(简化)来访问我的数据库。我知道使用语句和参数,但我缩短了它以集中我的问题。

string ConnectionString = "ConnectionStringHere";

//1. insert Transaction
SqlConnection myConnection = new SqlConnection(ConnectionString);            
SqlTransaction myTrans = myConnection.BeginTransaction();                
string sUpdate = "INSERT INTO User (FirstName, LastName) VALUES ('jon','doe')";
SqlCommand myCommand = new SqlCommand(sUpdate, myConnection, myTrans);
myCommand.ExecuteNonQuery();

//2. select from the same table
SqlConnection myConnection2 = new SqlConnection(ConnectionString);
string sSelect = "SELECT FirstName, LastName FROM User WHERE ID = 123";
DataTable dtResult = new DataTable();
SqlDataAdapter myDataAdapter2 = new SqlDataAdapter(sSelect, myConnection2);
myDataAdapter2.Fill(dtResult); //TimeOut Exception here

//submit changes from my transaction here
myTrans.Commit();

在第二部分中,我得到一个 TimeOutExcetion,因为我无法访问我的 User 表,直到我提交我的事务 myTrans.Commit(); 之后 - 死锁。

我的问题是 - 在这里避免死锁的最佳做法是什么?我可以通过将SELECT 作为事务的一部分或设置IsolationLevel

来避免异常
SqlTransaction myTrans = myConnection.BeginTransaction(IsolationLevel.ReadUncommitted);

但我不确定这些的用法。我不必担心无效数据,因为我总是按 ID 选择。

【问题讨论】:

你现在连连接都没有打开,是你的真实密码吗?使用using-statement @Tim,没有非真实代码 当前问题是由于使用单独的连接而出现的。这是你的要求吗? 如果您在同一个连接上执行 SELECT 会发生什么?我认为你不会陷入僵局。 @Arvo 好的,你有解释为什么会这样吗? 【参考方案1】:

我看不出您有任何理由将SELECT 查询作为事务的一部分来解决死锁或超时问题。在您认为的第一个 sql 连接 myConnection 上设置 ReadUncommitted 隔离级别也不是正确的方法。我看到有两种可能的解决方案:

    第一个解决方案:在您已启动的事务myTrans 上设置隔离级别IsolationLevel.ReadUncommitted 将无济于事。如果您对脏读感到满意,那么您实际上应该在第二个 SQL 连接 myConnection2 上设置此隔离级别,该连接是您为在 User 表上触发 select 查询而建立的。要通过myConnection2 设置select 查询的隔离级别,您需要使用with (nolock) 表级别提示。因此,您的查询将开始如下所示:

    string sSelect = "SELECT FirstName, LastName FROM User WITH (NOLOCK) WHERE ID = 123";
    

    您可以通过here获取更多详细信息。 另外,请阅读脏读here 的后果,这是使用此特定隔离级别的副作用。

    第二种解决方案:SQL Server 的默认隔离级别为Read Committed。因此,当您开始使用名为 myConnection2 的变量通过新 SQL 连接触发查询时,它正在使用 ReadCommitted 隔离级别。 ReadCommitted 隔离级别显示的默认行为是Blocking Read,即如果表上有未提交的更改(由于活动事务可以提交或回滚),那么您在User 表上的选择语句将被阻塞。它将等待事务完成,以便在回滚时可以读取新更新的数据或原始数据。如果它不执行这样的阻塞读取,那么它最终会执行dirty read,这是众所周知的数据库并发问题。 如果您不希望 SELECT 语句被阻塞并希望继续使用行的最后提交值,那么 SQL Server 中有一个名为 READ_COMMITTED_SNAPSHOT 的新数据库级别设置。以下是使用 SQL 脚本更改它的方法:

    ALTER DATABASE MyDatabase SET READ_COMMITTED_SNAPSHOT ON
    

    引用 Pinal Dave 的文章 here:

如果您在阅读器 (SELECT) 和 writers (INSERT/UPDATE/DELETE),那么你可以启用这个属性 无需更改应用程序中的任何内容。意思是 应用程序仍将在读取提交隔离下运行,并且将 仍然只读提交的数据。

注意:这是一个数据库级别设置,将影响使用READCOMMITTED隔离级别的数据库上的所有事务。

在我看来,您应该采用第一个解决方案。此外,您应该牢记几个关键点,以避免 SQL Server 查询中的死锁。引用 here 的 Pinal Dave 的话:

最小化交易规模和交易时间。 每次在应用程序中始终以相同的顺序访问服务器对象。 避免游标、while 循环或在运行时需要用户输入的进程。 减少应用程序的锁定时间。 尽可能使用查询提示来防止锁定(NoLock、RowLock) 使用 SET DEADLOCK_PRIORITY 选择死锁牺牲品。

【讨论】:

【参考方案2】:

这取决于您对该选择的需要。

如果您需要确保在选择之前进行了插入,您应该在同一个事务中执行这两个命令,或者在运行 SELECT 查询之前提交插入。

如果不是,那么这是SQL Server,你可以按照你说的设置隔离级别为READ UNCOMMITED,或者你可以使用NOLOCK table hint,即:

string sSelect = "SELECT FirstName, LastName FROM User WITH(NOLOCK) WHERE ID = 123";

但在您决定这样做之前,请在另一个问题中阅读它的优缺点:

When should you use "with (nolock)"

【讨论】:

【参考方案3】:

我不确定你为什么会遇到死锁,因为 sql server 默认会使用行锁,而且你想读取刚刚修改的相同行并不明显。

但这表示另一种选择是使用乐观并发级别,例如读取提交快照隔离 (RCSI) 或快照隔离 (snapshot)。如果原始表中的数据当前被锁定,这将使用 tempdb 存储读者可以访问的行版本。 使用快照,您必须在每个事务中显式设置事务隔离级别才能使用它......使用 RCSI,它是一个数据库级别设置。有关此主题的更多信息,请查看this article。

【讨论】:

以上是关于避免 SQL 事务中的死锁的主要内容,如果未能解决你的问题,请参考以下文章

在运行三足事务时避免死锁

事务 - 如何避免死锁?

mysql 死锁排查

mysql 开发进阶篇系列 14 锁问题(避免死锁,死锁查看分析)

(VIP-朝夕教育)2021-05-31 .NET高级班 28-数据库设计(事务,ACID,锁,避免死锁,存储过程)

避免与 Future 阻塞 发生死锁