避免 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 开发进阶篇系列 14 锁问题(避免死锁,死锁查看分析)