我在并发期间在应用程序中发现了一个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() + "';
我(客户端 - 服务器)应用程序的基本逻辑是这样的:
- 每当有人插入或在服务器端更新行,我也行插入ŧ他列表UpdatedRows,指定修改后的行的RowId。
- 当客户端尝试同步数据时,它首先将表ReceivedUpdatedRows中特定客户端的引用行中的所有行复制到表SynchronizingRows(第一条语句参与)在僵局中)。之后,在同步期间,我通过查找SynchronizingRows表来查找修改的行。这一步是必需的,否则如果有人在同步期间插入新行或修改服务器端的行,我将会错过它们,并且在下一次同步期间不会获取它们(解释场景需要很长时间才能写入...)。
- 同步完成后,我将行插入到ReceivedUpdatedRows表中,以指定此客户端已收到SynchronizingRows表(包含死锁的第二条语句)中包含的UpdatedRows。
- 最后,我删除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有什么影响吗?关闭它有什么负面的缺点吗?