You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SQL Server无锁Upsert方案探讨:错误处理与序列化隔离孰优?

Upsert方案:Serializable隔离级 vs 无锁TRY/CATCH的开销对比

首先,先梳理下你提到的两种Upsert方案,再针对你关心的开销问题展开分析:

常规Serializable隔离级方案

这个方案的核心是通过最高隔离级别避免并发插入冲突,但代价很明显:

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN TRANSACTION;
UPDATE dbo.table SET ... WHERE PK = @PK;
IF @@ROWCOUNT = 0 BEGIN
INSERT dbo.table(PK, ...)
END
COMMIT TRANSACTION;

Serializable隔离级会在查询时添加范围锁(你提到的表级锁是极端情况,更多是针对PK范围的键范围锁),这会导致高并发场景下大量事务阻塞排队,吞吐量急剧下降——毕竟它的设计目标是强一致性,而非高并发性能。

无锁TRY/CATCH方案

你提出的这个方案是典型的乐观并发控制思路,先尝试操作,冲突时再处理:

BEGIN TRANSACTION;
UPDATE dbo.table SET ... WHERE PK = @PK;
IF @@ROWCOUNT = 0 BEGIN
BEGIN TRY
INSERT dbo.table(PK, ...)
END TRY
BEGIN CATCH
IF ERROR_NUMBER() = 2627 BEGIN
UPDATE dbo.table SET ... WHERE PK = @PK;
END
END CATCH
END
COMMIT TRANSACTION;

逻辑很清晰:先更新,没找到行就插入;如果插入因为唯一键冲突(错误2627)失败,说明其他事务已经抢先插入了该行,这时再执行一次更新即可。

开销对比:错误处理 vs Serializable锁

你的疑惑点非常关键,这里分场景讨论:

1. 大多数操作是UPDATE(目标行已存在)

这种场景下,无锁方案几乎不会触发CATCH块,性能和普通UPDATE操作几乎一致。而Serializable方案会因为严格的锁机制,导致即使是普通UPDATE也会产生额外的锁开销,甚至引发阻塞——显然无锁方案更优。

2. 并发插入同一不存在PK的场景较多

此时无锁方案会频繁触发2627错误,进入CATCH块执行重试更新。错误处理确实有一定开销(比如捕获错误、检查错误号的逻辑),但对比Serializable隔离级下的锁竞争带来的事务排队阻塞,这种开销几乎可以忽略。

Serializable会让所有并发插入该PK的事务串行执行,相当于把并行操作变成了串行;而无锁方案是乐观尝试,只有在真正冲突时才重试,大部分时候事务可以并行完成,整体吞吐量会高很多。

额外的优化方案

其实还有一种折中的方案,不需要依赖错误处理,也能避免Serializable的强锁:使用UPDLOCK, HOLDLOCK提示来获取行级的更新锁,同时锁定PK范围防止并发插入:

BEGIN TRANSACTION;
UPDATE dbo.table WITH (UPDLOCK, HOLDLOCK) SET ... WHERE PK = @PK;
IF @@ROWCOUNT = 0 BEGIN
INSERT dbo.table(PK, ...)
END
COMMIT TRANSACTION;

这个方案的锁粒度比Serializable小很多,不会引发大范围阻塞,同时也能避免并发插入冲突,你可以把它也加入测试对比。

总的来说,高并发Upsert场景下,无锁TRY/CATCH方案的错误处理开销远低于Serializable隔离级带来的锁阻塞损失,是更优的选择。

内容的提问来源于stack exchange,提问作者Bruno Gonçalves

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.08 19:22:37