优先使用原子UPDATE+WHERE校验,因其轻量、可靠、易落地;相比SELECT+UPDATE可消除并发扣减超卖窗口,执行后须检查影响行数判断是否成功。

优先用原子 UPDATE + WHERE 条件校验,而不是先查后更新——这是最轻量、最可靠、也最容易落地的方案。
为什么原子 UPDATE WHERE 比 SELECT + UPDATE 更稳?
“先查库存是否够,再扣减”看似合理,但两个并发事务可能在毫秒级内都查到 stock = 1,接着都执行 UPDATE,结果变成 -1。这个时间窗口无法靠应用层 sleep 或重试堵住。
原子写法直接消除窗口:UPDATE products SET stock = stock - 1 WHERE id = 1001 AND stock >= 1。数据库引擎内部一次完成判断与修改,无需锁、不依赖隔离级别。
- 执行后必须检查影响行数:
@@ROWCOUNT(SQL Server)、ROW_COUNT()(MySQL)或pg_affected_rows()(PostgreSQL)为 0,说明条件不满足(已售罄或被他人抢先扣减) - 适用于判断逻辑能直接写进
WHERE的场景,比如“余额 ≥ X”、“状态 = 'pending'” - 避免在浮点字段上做精确值比对(如
WHERE price = 9.99),精度误差会导致条件失效
sp_getapplock 怎么调用才不拖垮系统?
sp_getapplock 是轻量级应用锁,但默认绑定事务——如果存储过程没正常 COMMIT 或 ROLLBACK,锁就一直挂着;连接池复用后,新请求会被静默阻塞,现象是“某条 SQL 突然卡死且无报错”。
- 锁操作必须包裹在
BEGIN TRANSACTION内,且不能在事务结束前退出存储过程 -
@Resource参数要带业务上下文,例如'order_pay_' + CAST(@order_id AS VARCHAR(20));硬编码'mylock'会锁住所有请求 -
@LockTimeout必须设具体毫秒值(如5000),别用默认-1(无限等待) - 必须检查返回值:
IF @result 表示失败(<code>-1=超时,-2=死锁牺牲品),不能只看@@ERROR
MERGE 语句适合哪些并发同步场景?
MERGE 的核心价值是“存在则更新、不存在则插入”的原子性,天然规避了 IF EXISTS + INSERT/UPDATE 的竞态。但它不是万能药,用错反而放大风险。
- ON 条件必须覆盖全部业务唯一键(如
email+tenant_id),不能只写主键;否则重复数据会漏判 - 源数据必须提前去重,否则报错
The MERGE statement attempted to UPDATE or DELETE the same row more than once - SQL Server 要求显式写出全部三个分支:
WHEN MATCHED、WHEN NOT MATCHED BY TARGET、WHEN NOT MATCHED BY SOURCE;漏一个就语法报错 - ON 字段必须有索引,否则百万级数据下性能断崖下跌;避免在 ON 中用函数(如
UPPER(t.email))
批量更新时怎么防死锁和日志暴涨?
单个事务更新几十万行,不是数据库扛不住,而是事务机制本身不允许:未提交事务会阻止日志截断、持续持有锁、触发锁升级(行锁→表锁),最终表现为日志使用率 >90%、log_reuse_wait_desc = ACTIVE_TRANSACTION、SSMS 卡死。
- 必须拆成小事务,每批最多 500–1000 行,用
TOP (500) ORDER BY id推进,别用OFFSET/FETCH(越往后越慢) - 每次更新后显式
COMMIT,不能依赖隐式提交;每批后加IF @@ROWCOUNT = 0 BREAK防死循环 - WHERE 条件字段(如
status = 'pending')必须建索引,否则每次都是全表扫描 - MySQL 里慎用变量模拟游标(如
@row_index := @row_index + 1),并发下极易漏行;建议由应用层加分布式锁,或用SELECT ... FOR UPDATE预占
真正容易被忽略的是:原子 UPDATE WHERE 和 MERGE 都要求业务逻辑能收敛到数据库可表达的条件里;一旦出现“需调用外部服务校验”或“状态流转含多步中间态”,就得退回到应用层协调,这时候锁或版本号就绕不开——但那已是另一个层面的设计问题了。

















