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

作者:Jeffcky 时间:2024-01-17 16:54:06 

前言

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

探讨存在就更新七种方案

首先我们来创建测试表


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

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

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

同时开启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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

经过多次认证也是零错误,貌似通过表变量形式实现可行。

解决方案七(提升隔离级别+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

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

来源:http://www.cnblogs.com/CreateMyself/p/6906594.html

标签:SQL,Server,并发处理,存在即更新
0
投稿

猜你喜欢

  • 如何将sql执行的错误消息记录到本地文件中实现过程

    2024-01-22 06:50:37
  • OpenCV 表盘指针自动读数的示例代码

    2023-12-29 05:46:38
  • python推导式的使用方法实例

    2021-03-13 09:38:25
  • sql之IN和BETWEEN条件运算

    2007-09-11 13:35:00
  • MySQL Select语句是如何执行的

    2024-01-22 20:05:44
  • python编写扎金花小程序的实例代码

    2021-01-28 23:09:44
  • Python深度学习之实现卷积神经网络

    2021-08-11 01:57:01
  • call在Python中改进数列的实例讲解

    2021-10-12 17:17:20
  • js实现复选框的全选和取消全选效果

    2024-05-02 17:40:02
  • 在Go语言中使用JSON的方法

    2024-04-25 15:26:04
  • Python简单删除列表中相同元素的方法示例

    2021-10-21 11:25:18
  • Linux下Python安装完成后使用pip命令的详细教程

    2021-07-07 02:45:51
  • Pytest测试报告工具Allure的高级用法

    2023-06-20 17:21:09
  • ASP程序与SQL存储过程结合使用详解

    2011-03-25 10:50:00
  • 让大家看看Object标签的强大功能---多用途

    2009-02-21 10:18:00
  • PHP循环与分支知识点梳理

    2023-05-29 13:09:21
  • 如何在Python函数执行前后增加额外的行为

    2023-05-27 17:35:20
  • js中函数声明与函数表达式

    2024-04-25 13:08:35
  • 常用原生JS兼容性写法汇总

    2024-04-22 22:38:15
  • 9种python web 程序的部署方式小结

    2021-04-14 10:39:44
  • asp之家 网络编程 m.aspxhome.com