我在併發期間在應用程序中發現了一個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有什麼影響嗎?關閉它有什麼負面的缺點嗎?