SELECT/UPDATE上的死锁

问题描述:

我在SQL Server 2008上的SELECT/UPDATE上遇到了死锁问题。 我从这个线程读取了答案:​​但我仍然不明白为什么我会死锁。SELECT/UPDATE上的死锁

我已经在下面的测试用例中重新创建了这种情况。

我有一个表:

CREATE TABLE [dbo].[SessionTest](
    [SessionId] UNIQUEIDENTIFIER ROWGUIDCOL NOT NULL, 
    [ExpirationTime] DATETIME NOT NULL, 
    CONSTRAINT [PK_SessionTest] PRIMARY KEY CLUSTERED (
     [SessionId] ASC 
    ) WITH (
     PAD_INDEX = OFF, 
     STATISTICS_NORECOMPUTE = OFF, 
     IGNORE_DUP_KEY = OFF, 
     ALLOW_ROW_LOCKS = ON, 
     ALLOW_PAGE_LOCKS = ON 
    ) ON [PRIMARY] 
) ON [PRIMARY] 
GO 

ALTER TABLE [dbo].[SessionTest] 
    ADD CONSTRAINT [DF_SessionTest_SessionId] 
    DEFAULT (NEWID()) FOR [SessionId] 
GO 

我想首先选择从这个表中的记录,如果记录存在设置过期时间为当前时间加上一些间隔。它使用下面的代码来实现:

protected Guid? GetSessionById(Guid sessionId, SqlConnection connection, SqlTransaction transaction) 
{ 
    Logger.LogInfo("Getting session by id"); 
    using (SqlCommand command = new SqlCommand()) 
    { 
     command.CommandText = "SELECT * FROM SessionTest WHERE SessionId = @SessionId"; 
     command.Connection = connection; 
     command.Transaction = transaction; 
     command.Parameters.Add(new SqlParameter("@SessionId", sessionId)); 

     using (SqlDataReader reader = command.ExecuteReader()) 
     { 
      if (reader.Read()) 
      { 
       Logger.LogInfo("Got it"); 
       return (Guid)reader["SessionId"]; 
      } 
      else 
      { 
       return null; 
      } 
     } 
    } 
} 

protected int UpdateSession(Guid sessionId, SqlConnection connection, SqlTransaction transaction) 
{ 
    Logger.LogInfo("Updating session"); 
    using (SqlCommand command = new SqlCommand()) 
    { 
     command.CommandText = "UPDATE SessionTest SET ExpirationTime = @ExpirationTime WHERE SessionId = @SessionId"; 
     command.Connection = connection; 
     command.Transaction = transaction; 
     command.Parameters.Add(new SqlParameter("@ExpirationTime", DateTime.Now.AddMinutes(20))); 
     command.Parameters.Add(new SqlParameter("@SessionId", sessionId)); 
     int result = command.ExecuteNonQuery(); 
     Logger.LogInfo("Updated"); 
     return result; 
    } 
} 

public void UpdateSessionTest(Guid sessionId) 
{ 
    using (SqlConnection connection = GetConnection()) 
    { 
     using (SqlTransaction transaction = connection.BeginTransaction(IsolationLevel.Serializable)) 
     { 
      if (GetSessionById(sessionId, connection, transaction) != null) 
      { 
       Thread.Sleep(1000); 
       UpdateSession(sessionId, connection, transaction); 
      } 
      transaction.Commit(); 
     } 
    } 
} 

然后,如果我试图从两个线程中执行测试方法,他们试图更新同一记录我获得以下的输出:

[4] : Creating/updating session 
[3] : Creating/updating session 
[3] : Getting session by id 
[3] : Got it 
[4] : Getting session by id 
[4] : Got it 
[3] : Updating session 
[4] : Updating session 
[3] : Updated 
[4] : Exception: Transaction (Process ID 59) was deadlocked 
on lock resources with another process and has been 
chosen as the deadlock victim. Rerun the transaction. 

我无法理解它可能会发生使用可序列化隔离级别。我认为首先选择应锁定行/表,不会让另一个选择获取任何锁。该示例是使用命令对象编写的,但它仅用于测试目的。最初,我使用LINQ,但我想展示简化的例子。 Sql Server Profiler显示死锁是键锁。我将在几分钟内更新问题,并从sql server profiler中发布图表。任何帮助,将不胜感激。我明白这个问题的解决方案可能是在代码中创建关键部分,但我试图理解为什么可序列化隔离级别无法做到这一点。

这里是死锁图形: deadlock http://img7.imageshack.us/img7/9970/deadlock.gif

在此先感谢。

+0

+1对于一个有据可查的问题! – 2009-06-06 11:44:16

它不足以具有可序列化的事务,您需要提示锁定以使其工作。

SERIALIZABLE隔离级别仍然通常会获得“最弱”类型的锁它能保证了串行化的条件被满足(重复读取,没有幻像行等)

所以,你正在抓住一个共享锁定的你的表在后面(在你的可串行化事务中)试图升级到an update lock.如果另一个线程持有共享锁(如果没有其他主体持有共享锁,它将工作),升级将失败。

你可能想将其更改为以下:

SELECT * FROM SessionTest with (updlock) WHERE SessionId = @SessionId 

这将确保一个更新锁在执行SELECT获取(这样你就不会需要升级锁)。

+0

它工作:)是否有可能实现这一点使用linq2sql? – empi 2009-06-06 11:53:10