探讨SQL Server并发处理存在就更新七种解决方案

本文涉及的产品
云数据库 RDS SQL Server,独享型 2核4GB
简介:

前言

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

探讨存在就更新七种方案

首先我们来创建测试表

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

在这种情况下更加糟糕,直接到会导致死锁 

 

此时将隔离级别提升为最高隔离级别会解决插入重复键问题,但是对于更新来获取排它锁而未提交,而此时另外一个进程进行查询获取共享锁此时将造成进程间相互阻塞从而造成死锁,所以从此知最高隔离级别有时候能够解决并发问题但是也会带来死锁问题。

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

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

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

 

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

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









本文转自Jeffcky博客园博客,原文链接:http://www.cnblogs.com/CreateMyself/p/6906594.html,如需转载请自行联系原作者

相关实践学习
使用SQL语句管理索引
本次实验主要介绍如何在RDS-SQLServer数据库中,使用SQL语句管理索引。
SQL Server on Linux入门教程
SQL Server数据库一直只提供Windows下的版本。2016年微软宣布推出可运行在Linux系统下的SQL Server数据库,该版本目前还是早期预览版本。本课程主要介绍SQLServer On Linux的基本知识。 相关的阿里云产品:云数据库RDS SQL Server版 RDS SQL Server不仅拥有高可用架构和任意时间点的数据恢复功能,强力支撑各种企业应用,同时也包含了微软的License费用,减少额外支出。 了解产品详情: https://www.aliyun.com/product/rds/sqlserver
目录
相关文章
|
8天前
|
SQL 人工智能 算法
【SQL server】玩转SQL server数据库:第二章 关系数据库
【SQL server】玩转SQL server数据库:第二章 关系数据库
51 10
|
1月前
|
SQL 数据库 数据安全/隐私保护
Sql Server数据库Sa密码如何修改
Sql Server数据库Sa密码如何修改
|
18天前
|
SQL
启动mysq异常The server quit without updating PID file [FAILED]sql/data/***.pi根本解决方案
启动mysq异常The server quit without updating PID file [FAILED]sql/data/***.pi根本解决方案
16 0
|
8天前
|
SQL 算法 数据库
【SQL server】玩转SQL server数据库:第三章 关系数据库标准语言SQL(二)数据查询
【SQL server】玩转SQL server数据库:第三章 关系数据库标准语言SQL(二)数据查询
68 6
|
4天前
|
SQL 安全 网络安全
IDEA DataGrip连接sqlserver 提示驱动程序无法通过使用安全套接字层(SSL)加密与 SQL Server 建立安全连接的解决方法
IDEA DataGrip连接sqlserver 提示驱动程序无法通过使用安全套接字层(SSL)加密与 SQL Server 建立安全连接的解决方法
8 0
|
9天前
|
SQL 存储 数据挖掘
数据库数据恢复—RAID5上层Sql Server数据库数据恢复案例
服务器数据恢复环境: 一台安装windows server操作系统的服务器。一组由8块硬盘组建的RAID5,划分LUN供这台服务器使用。 在windows服务器内装有SqlServer数据库。存储空间LUN划分了两个逻辑分区。 服务器故障&初检: 由于未知原因,Sql Server数据库文件丢失,丢失数据涉及到3个库,表的数量有3000左右。数据库文件丢失原因还没有查清楚,也不能确定数据存储位置。 数据库文件丢失后服务器仍处于开机状态,所幸没有大量数据写入。 将raid5中所有磁盘编号后取出,经过硬件工程师检测,没有发现明显的硬件故障。以只读方式将所有磁盘进行扇区级的全盘镜像,镜像完成后将所
数据库数据恢复—RAID5上层Sql Server数据库数据恢复案例
|
11天前
|
SQL 数据库 索引
SQL索引失效原因分析与解决方案
SQL索引失效原因分析与解决方案
19 0
|
12天前
|
SQL 数据安全/隐私保护
SQL Server 2016安装教程
SQL Server 2016安装教程
17 1
|
12天前
|
SQL 安全 Java
SQL server 2017安装教程
SQL server 2017安装教程
14 1
|
26天前
|
SQL 存储 Python
Microsoft SQL Server 编写汉字转拼音函数
Microsoft SQL Server 编写汉字转拼音函数