探討SQL Server併發處理存在就更新七種解決方案

前言

本節咱們來說講併發中最多見的狀況存在即更新,在併發中若未存在行記錄則插入,此時未處理好極容易出現插入重複鍵狀況,本文咱們來介紹對併發中存在就更新行記錄的七種方案而且咱們來綜合分析最合適的解決方案。html

探討存在就更新七種方案

首先咱們來建立測試表編程

IF OBJECT_ID('Test') IS NOT NULL
    DROP TABLE Test

CREATE TABLE Test
(
    Id int,
    Name nchar(100),
    [Counter] int,primary key (Id),
    unique (Name)
);
GO

解決方案一(開啓事務) 

咱們統一建立存儲過程經過來SQLQueryStress來測試併發狀況,咱們來看第一種狀況。併發

IF OBJECT_ID('TestPro') IS NOT NULL
    DROP PROCEDURE TestPro;
GO
 
CREATE PROCEDURE TestPro ( @Id INT )
AS
    DECLARE @Name NCHAR(100) = CAST(@Id AS NCHAR(100))
 
    BEGIN TRANSACTION
    IF EXISTS ( SELECT  1
                FROM    Test
                WHERE   Id = @Id )
        UPDATE  Test
        SET     [Counter] = [Counter] + 1
        WHERE   Id = @Id;
    ELSE
        INSERT  Test
                ( Id, Name, [Counter] )
        VALUES  ( @Id, @Name, 1 );
    COMMIT
GO

 

同時開啓100個線程和200個線程出現插入重複鍵的概率比較少仍是存在。高併發

解決方案二(下降隔離級別爲最低隔離級別UNCOMMITED)

IF OBJECT_ID('TestPro') IS NOT NULL
    DROP PROCEDURE TestPro;
GO
 
CREATE PROCEDURE TestPro ( @Id INT )
AS
    DECLARE @Name NCHAR(100) = CAST(@Id AS NCHAR(100))
 
    SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
    BEGIN TRANSACTION
    IF EXISTS ( SELECT  1
                FROM    Test
                WHERE   Id = @Id )
        UPDATE  Test
        SET     [Counter] = [Counter] + 1
        WHERE   Id = @Id;
    ELSE
        INSERT  Test
                ( Id, Name, [Counter] )
        VALUES  ( @Id, @name, 1 );
    COMMIT
GO

此時問題依舊和解決方案一無異(若是下降級別爲最低隔離級別,若是行記錄爲空,前一事務若是未進行提交,當前事務也能讀取到該行記錄爲空,若是當前事務插入進去並進行提交,此時前一事務再進行提交此時就會出現插入重複鍵問題)學習

解決方案三(提高隔離級別爲最高級別SERIALIZABLE)

IF OBJECT_ID('TestPro') IS NOT NULL
    DROP PROCEDURE TestPro;
GO
 
CREATE PROCEDURE TestPro ( @Id INT )
AS
    DECLARE @Name NCHAR(100) = CAST(@Id AS NCHAR(100))
 
    SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
    BEGIN TRANSACTION
    IF EXISTS ( SELECT  1
                FROM    dbo.Test
                WHERE   Id = @Id )
        UPDATE  dbo.Test
        SET     [Counter] = [Counter] + 1
        WHERE   Id = @Id;
    ELSE
        INSERT  dbo.Test
                ( Id, Name, [Counter] )
        VALUES  ( @Id, @Name, 1 );
    COMMIT
GO

在這種狀況下更加糟糕,直接到會致使死鎖 測試

 

此時將隔離級別提高爲最高隔離級別會解決插入重複鍵問題,可是對於更新來獲取排它鎖而未提交,而此時另一個進程進行查詢獲取共享鎖此時將形成進程間相互阻塞從而形成死鎖,因此今後知最高隔離級別有時候可以解決併發問題可是也會帶來死鎖問題。spa

解決方案四(提高隔離級別+良好的鎖)

此時咱們再來在添加最高隔離級別的基礎上增添更新鎖,以下:線程

IF OBJECT_ID('TestPro') IS NOT NULL
    DROP PROCEDURE TestPro;
GO
 
CREATE PROCEDURE TestPro ( @Id INT )
AS
    DECLARE @Name NCHAR(100) = CAST(@Id AS NCHAR(100))
 
    SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
    BEGIN TRANSACTION
    IF EXISTS ( SELECT  1
                FROM    dbo.Test WITH(UPDLOCK)
                WHERE   Id = @Id )
        UPDATE  dbo.Test
        SET     [Counter] = [Counter] + 1
        WHERE   Id = @Id;
    ELSE
        INSERT  dbo.Test
                ( Id, Name, [Counter] )
        VALUES  ( @Id, @Name, 1 );
    COMMIT
GO

 

運行屢次均未發現出現什麼異常,經過查詢數據時使用更新鎖而非共享鎖,這樣的話一來能夠讀取數據但不阻塞其餘事務,二來還確保自上次讀取數據後數據未被更改,這樣就解決了死鎖問題。貌似這樣的方案是可行得,若是是高併發不知是否可行。3d

解決方案五(提高隔離級別爲行版本控制SNAPSHOT) 

ALTER DATABASE UpsertTestDatabase
SET ALLOW_SNAPSHOT_ISOLATION ON
 
ALTER DATABASE UpsertTestDatabase
SET READ_COMMITTED_SNAPSHOT ON
GO 

IF OBJECT_ID('TestPro') IS NOT NULL
    DROP PROCEDURE TestPro;
GO
 
CREATE PROCEDURE TestPro ( @Id INT )
AS
    DECLARE @Name NCHAR(100) = CAST(@Id AS NCHAR(100))
 
    BEGIN TRANSACTION
    IF EXISTS ( SELECT  1
                FROM    dbo.Test
                WHERE   Id = @Id )
        UPDATE  dbo.Test
        SET     [Counter] = [Counter] + 1
        WHERE   Id = @Id;
    ELSE
        INSERT  dbo.Test
                ( Id, Name, [Counter] )
        VALUES  ( @Id, @Name, 1 );
    COMMIT
GO

上述解決方案也會出現插入重複鍵問題不可取。版本控制

解決方案六(提高隔離級別+表變量)

IF OBJECT_ID('TestPro') IS NOT NULL
    DROP PROCEDURE TestPro;
GO
 
CREATE PROCEDURE TestPro ( @Id INT )
AS
    DECLARE @Name NCHAR(100) = CAST(@Id AS NCHAR(100))
    DECLARE @updated TABLE ( i INT );
 
    SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
    BEGIN TRANSACTION
    UPDATE  Test
    SET     [Counter] = [Counter] + 1
    OUTPUT  DELETED.Id
            INTO @updated
    WHERE   Id = @Id;
 
    IF NOT EXISTS ( SELECT  i
                    FROM    @updated )
        INSERT  INTO Test
                ( Id, Name, counter )
        VALUES  ( @Id, @Name, 1 );
    COMMIT
GO

 

 

通過屢次認證也是零錯誤,貌似經過表變量形式實現可行。

解決方案七(提高隔離級別+Merge)

經過Merge關鍵來實現存在即更新不然則插入,同時咱們應該注意設置隔離級別爲 SERIALIZABLE  不然會出現插入重複鍵問題,代碼以下:

IF OBJECT_ID('TestPro') IS NOT NULL
    DROP PROCEDURE TestPro;
GO
 
CREATE PROCEDURE TestPro ( @Id INT )
AS
    DECLARE @Name NCHAR(100) = CAST(@Id AS NCHAR(100))
    SET TRAN ISOLATION LEVEL SERIALIZABLE 
    BEGIN TRANSACTION
    MERGE Test AS [target]
    USING
        ( SELECT    @Id AS Id
        ) AS source
    ON source.Id = [target].Id
    WHEN MATCHED THEN
        UPDATE SET
               [Counter] = [target].[Counter] + 1
    WHEN NOT MATCHED THEN
        INSERT ( Id, Name, [Counter] )
        VALUES ( @Id, @Name, 1 );
    COMMIT
GO

屢次認證不管是併發100個線程仍是併發200個線程依然沒有異常信息。

總結

本節咱們詳細討論了在併發中如何處理存在即更新,不然即插入問題的解決方案,目前來說以上三種方案可行。

解決方案一(最高隔離級別 + 更新鎖)

IF OBJECT_ID('TestPro') IS NOT NULL
    DROP PROCEDURE TestPro;
GO
 
CREATE PROCEDURE TestPro ( @Id INT )
AS
    DECLARE @Name NCHAR(100) = CAST(@Id AS NCHAR(100))
 
    BEGIN TRANSACTION;
 
    UPDATE  dbo.Test WITH ( UPDLOCK, HOLDLOCK )
    SET     [Counter] = [Counter] + 1
    WHERE   Id = @Id;
 
    IF ( @@ROWCOUNT = 0 )
        BEGIN
            INSERT  dbo.Test
                    ( Id, Name, [Counter] )
            VALUES  ( @Id, @Name, 1 );
        END
 
    COMMIT
GO

解決方案二(最高隔離級別 + 表變量)

IF OBJECT_ID('TestPro') IS NOT NULL
    DROP PROCEDURE TestPro;
GO
 
CREATE PROCEDURE TestPro ( @Id INT )
AS
    DECLARE @Name NCHAR(100) = CAST(@Id AS NCHAR(100))
    DECLARE @updated TABLE ( i INT );
 
    SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
    BEGIN TRANSACTION
    UPDATE  Test
    SET     [Counter] = [Counter] + 1
    OUTPUT  DELETED.id
            INTO @updated
    WHERE   id = @id;
 
    IF NOT EXISTS ( SELECT  i
                    FROM    @updated )
        INSERT  INTO Test
                ( Id, Name, counter )
        VALUES  ( @Id, @Name, 1 );
    COMMIT
GO

解決方案三(最高隔離級別 + Merge)

IF OBJECT_ID('TestPro') IS NOT NULL
    DROP PROCEDURE TestPro;
GO
 
CREATE PROCEDURE TestPro ( @Id INT )
AS
    DECLARE @Name NCHAR(100) = CAST(@Id AS NCHAR(100))
    SET TRAN ISOLATION LEVEL SERIALIZABLE 
    BEGIN TRANSACTION
    MERGE Test AS [target]
    USING
        ( SELECT    @Id AS Id
        ) AS source
    ON source.Id = [target].Id
    WHEN MATCHED THEN
        UPDATE SET
               [Counter] = [target].[Counter] + 1
    WHEN NOT MATCHED THEN
        INSERT ( Id, Name, [Counter] )
        VALUES ( @Id, @Name, 1 );
    COMMIT
GO

暫時只能想到這三種解決方案,我的比較推薦方案一和方案三, 請問您有何高見,請留下您的評論若可行,我將進行後續補充。

2017-06-03更新

本博文的評論很是精彩,同時對於小菜的我又從新學習了下存在即更新反之則插入的解決方案。本文從新更新已通過了兩天,期間我是一直在看這方面的東西更加深刻的理解有些基礎方面的東西仍是說的太籠統而且是我自身不是很理解而致使,菜不可怕,可怕的是還不深刻學習自認爲本身的是對的,你說呢。

首先咱們得理解UPDLOCK和HOLDLOCK鎖的做用是什麼,HOLDLOCK相似於SERIALIZABLE隔離級別,對於共享鎖咱們是能夠讀,可是不能進行更新和刪除和插入直到當前併發事務完成,而UPDLOCK園中博文的解釋:是容許您讀取數據(不阻塞其它事務)並在之後更新數據,同時確保自從上次讀取數據後數據沒有被更改。當咱們用它來讀取記錄時能夠對取到的記錄加上更新鎖,從而加上鎖的記錄在其它的線程中是不能更改的只能等本線程的事務結束後才能更改。通俗易懂點說,它不會阻塞併發的查詢和插入操做,可是會阻塞更新或者刪除對於當前事務查詢出的數據,當查詢到該數據存在時則有更新鎖切換到排它鎖。因此對於上述結尾總結的三種解決方案,咱們再來闡述下。

解決方案一(HOLDLOCK)

IF OBJECT_ID('TestPro') IS NOT NULL
    DROP PROCEDURE TestPro;
GO
 
CREATE PROCEDURE TestPro ( @Id INT )
AS
    DECLARE @Name NCHAR(100) = CAST(@Id AS NCHAR(100))
 
    BEGIN TRANSACTION;
 
    UPDATE  dbo.Test WITH ( HOLDLOCK )
    SET     [Counter] = [Counter] + 1
    WHERE   Id = @Id;
 
    IF ( @@ROWCOUNT = 0 )
        BEGIN
            INSERT  dbo.Test
                    ( Id, Name, [Counter] )
            VALUES  ( @Id, @Name, 1 );
        END
 
    COMMIT
GO

若是咱們未加上HOLDLOCK鎖提示,雖然UPDATE會獲取排它鎖,可是排它鎖不會持續到事務結束一直保持着因此會致使插入重複鍵的問題,當咱們加上HOLDLOCK鎖提示上述也說到相似悲觀併發中的最高隔離級別,該鎖提示一直會持續到事務結束,當有併發請求過來時,若此時查詢到數據存在則會進行更新操做可是事務還未進行提交,此時其餘請求將會也查到該行記錄存在,可是會被當前的事務更新操做鎖阻塞,若此時查詢到數據不存在時同理如此。

解決方案二(UPDLOCK + HOLDLOCK)

IF OBJECT_ID('TestPro') IS NOT NULL
    DROP PROCEDURE TestPro;
GO
 
CREATE PROCEDURE TestPro ( @Id INT )
AS
    DECLARE @Name NCHAR(100) = CAST(@Id AS NCHAR(100))
 
    BEGIN TRANSACTION
    IF EXISTS ( SELECT  1
                FROM    dbo.Test WITH(UPDLOCK, HOLDLOCK)
                WHERE   Id = @Id )
        UPDATE  dbo.Test
        SET     [Counter] = [Counter] + 1
        WHERE   Id = @Id;
    ELSE
        INSERT  dbo.Test
                ( Id, Name, [Counter] )
        VALUES  ( @Id, @Name, 1 );
    COMMIT
GO

對於上述查詢對比第一種解決方案咱們加上了UPDLOCK更新鎖代替SELECT的共享鎖,目的是當所傳遞的變量Id所查詢的行記錄不存在時不會致使阻塞,讓其進行插入,也就是說不阻塞其餘事務的插入並確保自上次以來行記錄未被修改過,對於HOLDLOCK爲了確保一直到事務釋放鎖,從而達到咱們的指望。總結起來一句話,若是查詢期間行記錄存在則鎖定的資源則查詢存在的行記錄上,若是查詢期間行記錄不存在,那麼經過HOLDLOCK來獲取主鍵上的範圍鎖來防止在釋放鎖以前插入重複鍵,因此UPDLOCK爲了解決併發更新不阻塞其餘事務查詢,HOLDLOCK防止併發插入重複鍵。

解決方案三(SERIALIZABLE + Merge)

IF OBJECT_ID('TestPro') IS NOT NULL
    DROP PROCEDURE TestPro;
GO
 
CREATE PROCEDURE TestPro ( @Id INT )
AS
    DECLARE @Name NCHAR(100) = CAST(@Id AS NCHAR(100))
    BEGIN TRANSACTION
    MERGE Test WITH(SERIALIZABLE ) AS [target]
    USING
        ( SELECT    @Id AS Id
        ) AS source
    ON source.Id = [target].Id
    WHEN MATCHED THEN
        UPDATE SET
               [Counter] = [target].[Counter] + 1
    WHEN NOT MATCHED THEN
        INSERT ( Id, Name, [Counter] )
        VALUES ( @Id, @Name, 1 );
    COMMIT
GO

結論

寫這篇博客後看到的評論才明白過來對併發存在即更新不然插入是隻知其一;不知其二,此前的我認爲更新語句獲取排它鎖,可是HOLDLOCK等同於SERIALIZABLE獲取共享鎖,可是排它鎖和共享鎖是互斥的,怎麼能夠在更新語句中添加HOLDLOCK提示呢?糾結了好久,只能說HOLDLOCK鎖提示的做用相似於SERIALIZABLE,一個屬於鎖,而SERIALIZABLE 屬於隔離級別,基於這點兩者是不同的,同時我一直認爲在存儲過程當中加上SERIALIZABLE隔離級別和語句中加上HOLDLOCK做用是同樣的,其實否則,在存儲過程當中加上隔離級別和語句中加上HOLDLOCK做用域不同,存儲過程當中加上SERIALIZABLE隔離級別對整個會話都起做用,而在語句中使用鎖提示只是對當前執行語句其做用,這裏感謝園友【笑東風】的指教,同時也感謝園友【MSSQL123】讓我明白了我混淆了鎖和隔離級別。

 

其實對於上述三種最終解決方案而言對於少許併發而言沒有什麼問題,上述對於存在即更新不然插入的併發方案只是下降了併發可能發生重複鍵的可能性,就像園友【Jacklondon Chen】所說,同時如上述第二種解決方案而言,若是行記錄不存在那麼UPDLOCK就不起做用,對於高併發而言利用HOLDLOCK雖然會阻塞插入,可是理論上來講估計依然會發生插入重複鍵的問題,此時推薦利用園友【Jacklondon Chen】的解決方案,園友專門寫了一篇博客來說述本篇博文的討論以做爲參考:http://www.cnblogs.com/jacklondon/p/programming_experience_concurrent.html

相關文章
相關標籤/搜索