数据库存在即更新的并发处理 - 转

前言

本节咱们来说讲并发中最多见的状况存在即更新,在并发中若未存在行记录则插入,此时未处理好极容易出现插入重复键状况,本文咱们来介绍对并发中存在就更新行记录的七种方案而且咱们来综合分析最合适的解决方案。并发

探讨存在就更新七种方案

首先咱们来建立测试表高并发

复制代码
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
复制代码

此时问题依旧和解决方案一无异(若是下降级别为最低隔离级别,若是行记录为空,前一事务若是未进行提交,当前事务也能读取到该行记录为空,若是当前事务插入进去并进行提交,此时前一事务再进行提交此时就会出现插入重复键问题)spa

解决方案三(提高隔离级别为最高级别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
复制代码

在这种状况下更加糟糕,直接到会致使死锁 线程

 

此时将隔离级别提高为最高隔离级别会解决插入重复键问题,可是对于更新来获取排它锁而未提交,而此时另一个进程进行查询获取共享锁此时将形成进程间相互阻塞从而形成死锁,因此今后知最高隔离级别有时候可以解决并发问题可是也会带来死锁问题。版本控制

解决方案四(提高隔离级别+良好的锁)

此时咱们再来在添加最高隔离级别的基础上增添更新锁,以下:code

复制代码
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
复制代码

 

运行屡次均未发现出现什么异常,经过查询数据时使用更新锁而非共享锁,这样的话一来能够读取数据但不阻塞其余事务,二来还确保自上次读取数据后数据未被更改,这样就解决了死锁问题。貌似这样的方案是可行得,若是是高并发不知是否可行。blog

解决方案五(提高隔离级别为行版本控制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

 

 

 

 

测试

UPDLOCK.UPDLOCK 的优势是容许您读取数据(不阻塞其它事务)并在之后更新数据,同时确保自从上次读取数据后数据没有被更改。当咱们用UPDLOCK来读取记录时能够对取到的记录加上更新锁,从而加上锁的记录在其它的线程中是不能更改的只能等本线程的事务结束后才能更改.

示例:

测试:

在另外一个查询里:

BEGIN TRANSACTION

SELECT * FROM myTable WITH (UPDLOCK) WHERE Id in (1,2,3)

waitfor delay '00:00:10' 


update myTable set [Name]='ZZ' where Id in (1,2,3)
commit TRANSACTION

在另外一个查询里:
SELECT * FROM myTable WHERE Id in (1,2,3)

能够立刻查询到数据。

但若是要更新数据,必须等其余更新锁释放后才能执行。

update myTable set [Name]='ZZ' where Id in (1,2,3)

这就说明,有时候须要控制某条记录在我读取后就不准再进行更新,那么我就能够将全部要处理当前记录的查询都加上更新锁,以防止查询后被其它事务修改.将事务的影响下降到最小

相关文章
相关标签/搜索