SQL Server Stored Procedure并发更新问题及锁方案咨询

结论先行
- 你当前用「事务+检查UPDATE受影响行数」的方案存在逻辑漏洞,且性能不是最优
- 完全可以在SELECT查询阶段直接锁定目标行,这是SQL Server处理「先查后改」并发场景的标准方案。
现有方案的问题
你现在的实现属于乐观并发思路,只有到最终UPDATE执行时才会触发数据库的行锁冲突,存在三个明显缺陷:
- 资源浪费:所有并发线程都会走完临时表数据校验、计算的全流程,最终只有一个线程能更新成功,其余线程的计算资源全部浪费,并发量越高损耗越明显
- 死锁风险高:多个并发事务同时持有不同资源的锁时,很容易在UPDATE阶段出现循环等待触发死锁,事务回滚成本很高
- 逻辑漏洞:如果你的UPDATE语句WHERE条件没有携带行版本标识(比如版本号、最后更新时间戳),仅靠主键匹配更新,在先提交的事务修改完行之后,后到的事务依然能成功UPDATE、返回
@@ROWCOUNT=1,根本识别不到重复更新,还是会出现覆盖问题。
推荐方案:SELECT阶段加更新锁
你只需要在初始查询目标行写入临时表时,加上UPDLOCK和HOLDLOCK两个表提示,就能在读取阶段直接锁定待更新的行,从根源避免并发线程读到同一批数据,参考写法:
BEGIN TRANSACTION; -- 读取目标行时直接加锁,其他并发同逻辑查询会在此处阻塞等待 SELECT 你的查询字段 INTO #TempCalcTable FROM 业务目标表 WITH (UPDLOCK, HOLDLOCK) WHERE 初始查询筛选条件; -- 后续正常做临时表的数据校验、逻辑计算即可,不会有其他线程能修改你查到的这批行 IF -- 校验不通过的判断逻辑 BEGIN ROLLBACK TRANSACTION; RETURN 错误状态码; END -- 执行更新 UPDATE 业务目标表 SET 字段 = 计算后的值 WHERE 主键 IN (SELECT 主键 FROM #TempCalcTable); -- 保留受影响行数检查作为兜底校验 IF @@ROWCOUNT <> 预期更新行数 BEGIN ROLLBACK TRANSACTION; RETURN 重复更新错误码; END COMMIT TRANSACTION;
锁提示说明
UPDLOCK:对读取的行加更新锁,更新锁与普通SELECT的共享锁兼容,但与其他更新锁、排他锁互斥,保证同一时间只有一个线程能拿到目标行的更新权限,同时可以避免读锁升级写锁时常见的死锁问题HOLDLOCK:等价于SERIALIZABLE隔离级别提示,会让持有的锁一直保留到事务提交/回滚后才释放,避免查询语句执行完就提前放锁导致的并发穿透。
性能注意事项
- 初始查询的WHERE筛选条件必须命中主键或唯一索引,否则SQL Server无法精准加行锁,会升级为范围锁甚至表锁,直接拖垮整体并发性能
- 尽量缩小事务范围,不要在事务内执行无关的耗时操作(比如外部接口调用、非必要的大批量计算),减少锁持有时间
- 原有的
@@ROWCOUNT检查逻辑可以保留作为兜底,但不需要作为主要的并发控制手段。
这种悲观锁方案在你描述的场景下性能远好于现有乐观校验方案:并发线程会在查询第一步就排队,不会执行无用的前置计算,死锁概率极低,也不会出现重复更新覆盖的问题。
内容的提问来源于stack exchange,提问作者Mohit
相关产品推荐
相关产品推荐

