2010-12-11 47 views
1

我在并发期间在应用程序中发现了一个SQL死锁场景。我相信这两种说法引起死锁是(注意 - 我使用LINQ2SQL和DataContext.ExecuteCommand(),这就是this.studioId.ToString()进场):哪种解决方法可用于以下SQL死锁?

exec sp_executesql N'INSERT INTO HQ.dbo.SynchronizingRows ([StudioId], [UpdatedRowId]) 
SELECT @p0, [t0].[Id] FROM [dbo].[UpdatedRows] AS [t0] WHERE NOT (EXISTS( 
SELECT NULL AS [EMPTY] FROM [dbo].[ReceivedUpdatedRows] AS [t1] WHERE ([t1].[StudioId] = @p0) 
AND ([t1].[UpdatedRowId] = [t0].[Id])))',N'@p0 uniqueidentifier',@p0='" + this.studioId.ToString() + "'; 

exec sp_executesql N'INSERT INTO HQ.dbo.ReceivedUpdatedRows ([UpdatedRowId], [StudioId], [ReceiveDateTime]) 
SELECT [t0].[UpdatedRowId], @p0, GETDATE() FROM [dbo].[SynchronizingRows] AS [t0] 
WHERE ([t0].[StudioId] = @p0)',N'@p0 uniqueidentifier',@p0='" + this.studioId.ToString() + "'; 

我(客户端 - 服务器)应用程序的基本逻辑是这样的:

  1. 每当有人插入或在服务器端更新行,我也行插入ŧ他列表UpdatedRows,指定修改后的行的RowId。
  2. 当客户端尝试同步数据时,它首先将表ReceivedUpdatedRows中特定客户端的引用行中的所有行复制到表SynchronizingRows(第一条语句参与)在僵局中)。之后,在同步期间,我通过查找SynchronizingRows表来查找修改的行。这一步是必需的,否则如果有人在同步期间插入新行或修改服务器端的行,我将会错过它们,并且在下一次同步期间不会获取它们(解释场景需要很长时间才能写入...)。
  3. 同步完成后,我将行插入到ReceivedUpdatedRows表中,以指定此客户端已收到SynchronizingRows表(包含死锁的第二条语句)中包含的UpdatedRows。
  4. 最后,我删除SynchronizingRows表中属于当前客户端的所有行。

我看到它的方式,死锁上表SynchronizingRows(简称SR)和ReceivedUpdatedRows(缩写RUR)中的步骤2和3(一个客户端是在步骤2中存在的并且被插入到SR和从RUR选择;而另一个客户在步骤3中插入RUR并从SR中选择)。

我搜索了一下SQL死锁,并得出结论,我有三个选择。 中序做出决定,我需要每个选项/解决方法的详细输入:

解决方法1:

在网络上给出有关SQL死锁的第一个忠告 - 重组表/查询,以便死锁别首先发生。唯一的问题是,用我的智商我没有看到任何不同的方式来执行同步逻辑。如果有人希望更深入地了解我目前的同步逻辑,如何设置它的方式和原因,我会发布一个链接来解释。也许,在比我聪明的人的帮助下,可以创建一个无死锁的逻辑。

解决方法2:

第二个最常见的意见似乎是使用WITH(NOLOCK)的暗示。这个问题是,NOLOCK可能会丢失或重复某些行。复制不是问题,但缺少行是灾难性的!另一个选项是WITH(READPAST)提示。表面看来,这似乎是一个完美的解决方案。我真的不关心其他客户端插入/修改的行,因为每行只属于特定的客户端,所以我可能会跳过锁定的行。但MSDN文档让我有些担心 - “当指定READPAST时,行级锁和页级锁均被跳过”。正如我所说的,行级锁不会成为问题,但页级锁可能非常适用,因为页面可能包含属于多个客户端(包括当前的)的行。

虽然有很多博客文章特别提到NOLOCK可能会漏掉行,但似乎没有关于READPAST(从不)缺少的行。这使我对实现它感到怀疑和紧张,因为没有简单的方法来测试它(实现将是小菜一碟,只需将WITH(READPAST)插入到SELECT子句和完成的任务中)。 有人可以确认READPAST提示是否可以错过行吗?

解决方法3:

最后的选择是使用ALLOW_SNAPSHOT_ISOLATION和READ_COMMITED_SNAPSHOT。这似乎是100%工作的唯一选择 - 至少我找不到任何与它相矛盾的信息。但是安装起来有点麻烦(我不关心性能问题),因为我在使用LINQ。关闭我的头,我可能需要手动打开一个SQL连接并将它传递给LINQ2SQL DataContext,等等......我没有深入研究具体细节。

如果somone只能让我确信READPAST不会错过关于当前客户端的行(正如我之前所说的,每个客户端都有且只处理它自己的一组行),我通常更希望选项2。否则,我很可能不得不实现选项3,因为选项1大概是不可能的......

我会后三个表的表定义为好,以防万一:

CREATE TABLE [dbo].[UpdatedRows](
    [Id] [uniqueidentifier] NOT NULL ROWGUIDCOL DEFAULT NEWSEQUENTIALID() PRIMARY KEY CLUSTERED, 
    [RowId] [uniqueidentifier] NOT NULL, 
    [UpdateDateTime] [datetime] NOT NULL, 
) ON [PRIMARY] 
GO 

CREATE NONCLUSTERED INDEX IX_RowId ON dbo.UpdatedRows 
    ([RowId] ASC) WITH (STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] 
GO 

CREATE TABLE [dbo].[ReceivedUpdatedRows](
    [Id] [uniqueidentifier] NOT NULL ROWGUIDCOL DEFAULT NEWSEQUENTIALID() PRIMARY KEY NONCLUSTERED, 
    [UpdatedRowId] [uniqueidentifier] NOT NULL REFERENCES [dbo].[UpdatedRows] ([Id]), 
    [StudioId] [uniqueidentifier] NOT NULL REFERENCES, 
    [ReceiveDateTime] [datetime] NOT NULL, 
) ON [PRIMARY] 
GO 

CREATE CLUSTERED INDEX IX_Studios ON dbo.ReceivedUpdatedRows 
    ([StudioId] ASC) WITH (STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] 
GO 

CREATE TABLE [dbo].[SynchronizingRows](
    [StudioId] [uniqueidentifier] NOT NULL 
    [UpdatedRowId] [uniqueidentifier] NOT NULL REFERENCES [dbo].[UpdatedRows] ([Id]) 
    PRIMARY KEY CLUSTERED ([StudioId], [UpdatedRowId]) 
) ON [PRIMARY] 
GO 

PS! Studio =客户端。
PS2!我只注意到索引定义有ALLOW_PAGE_LOCK = ON。如果我将它关闭,这会对READPAST有什么影响吗?关闭它有什么负面的缺点吗?

回答

0

我们终于设法解决了我的问题。

我没有使用解决方法2,因为跳过页面会导致问题,我不想冒这个风险。

我没有使用解决方法3,因为快照隔离是针对每个数据库的。这意味着所有的表都会受到影响,并且每行都会变大14个字节(我认为),对我来说这不是一个可行的选择。

我选择了解决方法1 - 我在我的sp_executesql语句周围创建了一个全局互斥锁。这有利于我100%保证一切正常,但它当然不会扩展。目前这不是问题,因为拥堵并不是那么大。虽然我仍然想找到一种方法来并行执行有问题的sql语句,但不会导致死锁并且不会丢失属于指定studio的行。所以如果有人有任何想法,请随时回答/评论。我确实想过要删除受影响表上的所有索引,因为我确信死锁也与索引有关,但我最终可能会得到完整的表锁,这并不是事实上它更糟糕,因为在我的互斥体解决方案中,我可以使用索引并避免全表扫描,这是更快的。)