原子update+where比显式加锁更直接有效,能堵死并发冲突时间窗;必须检查影响行数,避免浮点比较;select for update需索引与事务配合;sql server应使用updlock+rowlock;批量更新需警惕锁范围失控。

UPDATE 带 WHERE 条件校验比加锁更直接有效
多数并发冲突(如超卖、余额负值)根本不需要显式加锁——用原子 UPDATE + WHERE 条件就能堵死“先查后更新”的时间窗口。数据库在执行时会先加行锁、再读最新值、最后判断条件,整个过程不可分割。
- 正确写法:
UPDATE goods SET stock = stock - 1 WHERE id = 123 AND stock >= 1 - 执行后必须检查影响行数:
ROW_COUNT()(MySQL)、@@ROWCOUNT(SQL Server)、pg_affected_rows()(PostgreSQL)为 0 表示失败,不是报错 - 避免浮点字段做精确比较(如
price = 9.99),受精度影响可能导致条件恒假 - 不依赖隔离级别,
autocommit=1下也生效,无需 BEGIN/COMMIT 包裹
SELECT FOR UPDATE 必须配合索引和事务才真正起作用
SELECT ... FOR UPDATE 不是“一写就锁”,它只在事务内、且查询走索引时才加行锁;否则可能升级为表锁或间隙锁,反而放大阻塞面。
- 必须显式开启事务:
BEGIN或START TRANSACTION,否则锁在语句结束就释放 - WHERE 字段必须命中索引:
EXPLAIN中type应为const、ref或range,key显示具体索引名 - 在
REPEATABLE READ下,即使查不到记录也会加间隙锁(如WHERE order_no = 'ABC'),多个并发请求容易死锁 - MySQL 8.0.19+ 支持单表
UPDATE ... ORDER BY,但仅当EXPLAIN验证走索引时,才保证加锁顺序一致
SQL Server 的 UPDLOCK + ROWLOCK 是显式悲观锁的可靠写法
SELECT WITH (UPDLOCK, ROWLOCK) 是 SQL Server 中真正能阻塞并发更新的操作,它不靠触发器、也不靠事务隔离级别“自动生效”,而是明确告诉引擎:“我要更新这行,现在就锁住”。
- 必须在事务中使用,且
UPDATE要紧随其后,中间不能有其他语句干扰锁上下文 -
UPDLOCK防止其他事务获取共享锁或更新锁;ROWLOCK抑制锁升级成页锁或表锁——两个 hint 缺一不可 - 错误示范:
SELECT WITH (HOLDLOCK)只持共享锁,别人仍可UPDATE;SELECT WITH (TABLOCK)直接锁整表 - AFTER 触发器无法替代它:触发器执行时更新已完成,冲突早已发生;INSTEAD OF 触发器不持有锁,也不阻塞其他会话
批量 UPDATE 时最容易忽略的锁范围失控点
你以为只锁了 100 行,实际可能锁了上万行间隙,甚至被 DDL 卡住——问题常出在范围扫描、索引缺失或 ORDER BY 失效上。
- 别信
LIMIT:MySQL 的UPDATE ... LIMIT 100不保证按主键顺序加锁,若没索引支撑,优化器可能全表扫描并随机加锁 - 游标优于 OFFSET:
WHERE id > 15000 AND id 比 <code>LIMIT 100 OFFSET 15000更可控,避免数据变动导致重复或遗漏 - 联合索引要覆盖查询+排序:
WHERE status = 'pending' AND created_at >= '2026-09-01'就该建INDEX idx_status_ctime_id (status, created_at, id),让 next-key 锁收敛在窄区间 - READ COMMITTED 隔离级别下间隙锁失效,但仅限等值查询命中唯一索引;范围条件(
>,BETWEEN)仍可能锁大量间隙
真正难处理的从来不是“怎么加锁”,而是“怎么确认锁加在了哪里、加了多少、为什么没按预期释放”——建议上线前用 SHOW ENGINE INNODB STATUS(MySQL)、pg_locks(PostgreSQL)或 sys.dm_tran_locks(SQL Server)实测验证,而不是只看文档描述。











