SQL Server并发处理存在就更新解决方案探讨
前言
本节我们来讲讲并发中最常见的情况存在即更新,在并发中若未存在行记录则插入,此时未处理好极容易出现插入重复键情况,本文我们来介绍对并发中存在就更新行记录的七种方案并且我们来综合分析最合适的解决方案。
探讨存在就更新七种方案
首先我们来创建测试表
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
暂时只能想到这三种解决方案,个人比较推荐方案一和方案三, 请问您有何高见,请留下您的评论若可行,我将进行后续补充。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。
- 老司机带你过常规WAF
- Git 项目推荐 | javascript ajax 代理调用工具
- Hive 时间日期处理总结
- 孤立的SQL用户
- 如何卸载CDH(附一键卸载github源码)
- github & CSRF
- 如何使用Python读取大文件
- 介绍一种非常好用汇总数据的方式GROUPING SETS
- 史上最大的CPU Bug(幽灵和熔断的OS&SQLServer补丁)
- 数据库副本的自动种子设定(自增长)
- Git 项目推荐 | 基于go+protobuff 实现的分布式
- ReflectASM-invoke,高效率java反射机制原理
- Web应用渗透测试-本地文件包含
- shiro权限控制(二):分布式架构中shiro的实现
- MySQL 教程
- MySQL 安装
- MySQL 管理与配置
- MySQL PHP 语法
- MySQL 连接
- MySQL 创建数据库
- MySQL 删除数据库
- MySQL 选择数据库
- MySQL 数据类型
- MySQL 创建数据表
- MySQL 删除数据表
- MySQL 插入数据
- MySQL 查询数据
- MySQL where 子句
- MySQL UPDATE 查询
- MySQL DELETE 语句
- MySQL LIKE 子句
- mysql order by
- Mysql Join的使用
- MySQL NULL 值处理
- MySQL 正则表达式
- MySQL 事务
- MySQL ALTER命令
- MySQL 索引
- MySQL 临时表
- MySQL 复制表
- 查看MySQL 元数据
- MySQL 序列 AUTO_INCREMENT
- MySQL 处理重复数据
- MySQL 及 SQL 注入
- MySQL 导出数据
- MySQL 导入数据
- MYSQL 函数大全
- MySQL Group By 实例讲解
- MySQL Max()函数实例讲解
- mysql count函数实例
- MYSQL UNION和UNION ALL实例
- MySQL IN 用法
- MySQL between and 实例讲解
- Python3 实现建造者模式
- python 实现原型设计模式
- python 最简单的实现适配器设计模式
- python3 最基本且简单的实现组合设计模式
- python 实现装饰器设计模式
- 看得懂的外观设计模式 python3 实现
- 看得懂的设计模式 享元模式python3 最基本(简单)实现
- python3 最简单的实现 模版设计模式
- Qt 第二步 槽与信号(一) 实现点击按钮并弹窗
- python3 爬虫第一步 简单获取网页基本信息
- python3 爬虫第二步Selenium 使用简单的方式抓取复杂的页面信息
- Qt 第二步 熟悉文件结构组成(二)
- php基础教程 第一步 环境配置及helloworld
- 推荐两个Docker配置检查与启动异常修复方法脚本
- 持续集成gitlab-ci.yml配置文档基础