SQL并发执行Insert-If-Not-Exists存储过程遇死锁,如何让进程等待而非成为受害者?
解决并发存储过程的死锁问题:让进程等待而非成为死锁受害者
嘿,这个问题我帮不少开发者踩过坑,咱们一步步来理清楚:
首先,这不是设计使然,是你的存储过程逻辑有缺陷
先看你的代码结构:
IF Exists () Select Else Set Transaction isolation level serializable Begin Tran If not exists() Insert Select Commit Tran
这种“先在事务外检查Exists,再进入事务重复检查并插入”的模式,在高并发场景下天生容易触发死锁,原因是:
- 外层的
Exists()查询会在Serializable隔离级别下加共享锁,多个进程都能通过这一步; - 接着多个进程同时进入事务,尝试执行
Insert操作,这时候需要把共享锁升级为排他锁; - 每个进程都在等待其他进程释放共享锁,形成了循环等待——这就是死锁的核心原因,SQL Server会选择一个“受害者”终止进程来打破死锁。
当然可以让第二个进程等待,甚至避免死锁,这里给你几个可行方案:
方案1:用原子化的“检查+插入”语句,彻底避免死锁
把事务外的检查和事务内的操作合并成一个原子SQL语句,让SQL Server一次性处理检查和插入,这样不会出现中间的锁竞争:
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; BEGIN TRY BEGIN TRANSACTION; -- 原子操作:检查不存在就插入,一步完成 INSERT INTO YourTargetTable (Column1, Column2) SELECT @Param1, @Param2 WHERE NOT EXISTS ( SELECT 1 FROM YourTargetTable WHERE Column1 = @Param1 -- 这里是你的唯一判断条件 ); -- 如果需要返回插入后的结果,可以在这里加Select SELECT * FROM YourTargetTable WHERE Column1 = @Param1; COMMIT TRANSACTION; END TRY BEGIN CATCH IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION; -- 捕获死锁错误(错误号1205),加入重试逻辑 IF ERROR_NUMBER() = 1205 BEGIN -- 可以设置重试次数,比如最多重试3次 RAISERROR('死锁发生,正在重试...', 10, 1) WITH NOWAIT; WAITFOR DELAY '00:00:01'; -- 等待1秒后重试 -- 这里可以通过循环或递归实现重试逻辑,根据你的业务需求调整 END ELSE BEGIN -- 抛出其他错误,方便排查 THROW; END END CATCH
方案2:调整死锁优先级,降低成为受害者的概率
如果你的场景中必须保留原有逻辑,可以设置死锁优先级,让当前进程更不容易被选为受害者:
-- 在存储过程开头设置死锁优先级为高(默认是NORMAL) SET DEADLOCK_PRIORITY HIGH;
不过这个方案只能降低概率,不能从根源避免死锁,所以更推荐方案1。
方案3:统一资源访问顺序
如果你的存储过程涉及多个表,确保所有进程都按照相同的顺序访问这些表(比如先访问表A,再访问表B),这样可以打破循环等待的条件,避免死锁。
总结
死锁不是必然的,是你的“先读后写”逻辑在并发下的产物。用原子化的SQL语句是最彻底的解决方式,配合TRY/CATCH的重试逻辑,就能让进程在遇到锁竞争时等待,而不是直接被终止。
内容的提问来源于stack exchange,提问作者j doe
相关产品推荐
相关产品推荐

