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

发布时间 - 2026-01-11 01:37:50    点击率:

前言

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

探讨存在就更新七种方案

首先我们来创建测试表

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

暂时只能想到这三种解决方案,个人比较推荐方案一和方案三, 请问您有何高见,请留下您的评论若可行,我将进行后续补充。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。


# SQL  # Server  # 并发处理  # 存在即更新  # mysql如果数据不存在  # 则插入新数据  # 否则更新的实现方法  # mysql 存在该记录则更新  # 不存在则插入记录的sql  # mysql 记录不存在时插入 记录存在则更新的实现方法  # 死锁  # 您的  # 也会  # 我将  # 有何  # 时将  # 七种  # 为空  # 本节  # 这三种  # 请问您  # 就会  # 基础上  # 也能  # 请留下  # 再来  # 三种  # 另外一个  # 而非  # 我们应该 


相关栏目: 【 网站优化151355 】 【 网络推广146373 】 【 网络技术251813 】 【 AI营销90571


相关推荐: Laravel如何监控和管理失败的队列任务_Laravel失败任务处理与监控  深圳网站制作设计招聘,关于服装设计的流行趋势,哪里的资料比较全面?  Laravel PHP版本要求一览_Laravel各版本环境要求对照  郑州企业网站制作公司,郑州招聘网站有哪些?  齐河建站公司:营销型网站建设与SEO优化双核驱动策略  Java垃圾回收器的方法和原理总结  Laravel Asset编译怎么配置_Laravel Vite前端构建工具使用  Laravel如何部署到服务器_线上部署Laravel项目的完整流程与步骤  Laravel队列任务超时怎么办_Laravel Queue Timeout设置详解  EditPlus中的正则表达式实战(6)  JavaScript如何实现路由_前端路由原理是什么  Laravel怎么做缓存_Laravel Cache系统提升应用速度的策略与技巧  Laravel怎么使用Collection集合方法_Laravel数组操作高级函数pluck与map【手册】  如何用腾讯建站主机快速创建免费网站?  韩国代理服务器如何选?解析IP设置技巧与跨境访问优化指南  Laravel 419 page expired怎么解决_Laravel CSRF令牌过期处理  谷歌浏览器下载文件时中断怎么办 Google Chrome下载管理修复  胶州企业网站制作公司,青岛石头网络科技有限公司怎么样?  JavaScript数据类型有哪些_如何准确判断一个变量的类型  Laravel N+1查询问题如何解决_Eloquent预加载(Eager Loading)优化数据库查询  高端企业智能建站程序:SEO优化与响应式模板定制开发  Laravel Telescope怎么调试_使用Laravel Telescope进行应用监控与调试  千问怎样用提示词获取健康建议_千问健康类提示词注意事项【指南】  Laravel的.env文件有什么用_Laravel环境变量配置与管理详解  Laravel观察者模式如何使用_Laravel Model Observer配置  如何在不使用负向后查找的情况下匹配特定条件前的换行符  如何用y主机助手快速搭建网站?  Laravel Eloquent:优雅地将关联模型字段扁平化到主模型中  如何在VPS电脑上快速搭建网站?  canvas 画布在主流浏览器中的尺寸限制详细介绍  打开php文件提示内存不足_怎么调整php内存限制【解决方案】  Laravel如何使用Eloquent进行子查询  Win11关机界面怎么改_Win11自定义关机画面设置【工具】  Google浏览器为什么这么卡 Google浏览器提速优化设置步骤【方法】  个人摄影网站制作流程,摄影爱好者都去什么网站?  PHP的CURL方法curl_setopt()函数案例介绍(抓取网页,POST数据)  北京的网站制作公司有哪些,哪个视频网站最好?  Laravel如何使用API Resources格式化JSON响应_Laravel数据资源封装与格式化输出  零基础网站服务器架设实战:轻量应用与域名解析配置指南  php后缀怎么变mp4格式错误_修改扩展名提示格式不对怎么办【技巧】  如何在景安服务器上快速搭建个人网站?  Laravel如何创建自定义中间件?(Middleware代码示例)  微信小程序 HTTPS报错整理常见问题及解决方案  如何在万网开始建站?分步指南解析  如何用ChatGPT准备面试 模拟面试问答与职场话术练习教程  如何制作新型网站程序文件,新型止水鱼鳞网要拆除吗?  Laravel如何处理CORS跨域请求?(配置示例)  独立制作一个网站多少钱,建立网站需要花多少钱?  如何彻底删除建站之星生成的Banner?  JS中对数组元素进行增删改移的方法总结