2010-12-11 74 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的行。所以如果有人有任何想法,請隨時回答/評論。我確實想過要刪除受影響表上的所有索引,因爲我確信死鎖也與索引有關,但我最終可能會得到完整的表鎖,這並不是事實上它更糟糕,因爲在我的互斥體解決方案中,我可以使用索引並避免全表掃描,這是更快的。)