mysql 8.0.1+ 支持 select ... for update nowait,遇锁冲突立即报错 er_lock_wait_timeout(1205),不等待;它不能用于 update 语句本身,须先 select 加锁再 update,且必须在同一事务中执行。

MySQL 8.0+ 的 SELECT ... FOR UPDATE NOWAIT 是什么
它让 SELECT ... FOR UPDATE 在遇到锁冲突时不等待,而是立刻报错 Lock wait timeout exceeded(实际是更明确的 ER_LOCK_WAIT_TIMEOUT 错误码 1205),避免事务长时间挂起。注意:MySQL 原生不支持 NOWAIT 关键字,直到 8.0.1 才正式加入 —— 低于这个版本直接写会报语法错误 ERROR 1064。
常见误解是以为 NOWAIT 能“跳过锁”或“降级为读”,其实它只是把阻塞行为改为立即失败,控制权交还给应用层做重试、降级或提示。
为什么 NOWAIT 不能用于 UPDATE 语句本身
MySQL 不允许在 UPDATE 后直接加 NOWAIT,例如 UPDATE t SET x=1 WHERE id=1 NOWAIT 会触发 ERROR 1064。这是语法限制,不是配置问题。
必须拆成两步:
- 先用
SELECT ... FOR UPDATE NOWAIT尝试加锁 - 再执行
UPDATE(此时已持有行锁,不会阻塞)
中间不能有其他事务释放锁,所以这两步需在同一事务内完成。否则 SELECT 成功但 UPDATE 时锁已被释放,仍可能遇到并发更新覆盖或死锁。
实战中容易踩的三个坑
NOWAIT 看似简单,但线上出问题往往卡在这几个点:
-
隔离级别影响可见性:在
READ COMMITTED下,SELECT ... FOR UPDATE NOWAIT可能查不到最新已提交的行(因快照读),导致误判“无数据可锁”,建议统一用REPEATABLE READ -
没开事务就用
FOR UPDATE:MySQL 会自动开启隐式事务,但语句执行完即提交,锁立刻释放 ——NOWAIT失去意义;务必显式BEGIN/START TRANSACTION -
WHERE 条件未命中索引:会导致锁住整个表或多个无关行,
NOWAIT报错后你看到的是“锁表失败”,而非“某行被占”,排查成本陡增;用EXPLAIN确认走了主键或唯一索引
一个最小可用的热点更新保护片段
假设要安全更新用户余额,防止超扣:
START TRANSACTION; SELECT balance FROM users WHERE user_id = 123 FOR UPDATE NOWAIT; -- 如果这里报错 ER_LOCK_WAIT_TIMEOUT(1205),应用应捕获并重试/告警 UPDATE users SET balance = balance - 100 WHERE user_id = 123 AND balance >= 100; -- 检查 ROW_COUNT() 是否为 1,防止条件不满足却静默成功 COMMIT;
关键点:SELECT ... FOR UPDATE NOWAIT 必须和后续 UPDATE 在同一事务;UPDATE 中重复写 WHERE 条件(尤其是金额校验),避免锁住了却更新了错误状态;不要依赖 SELECT 返回值做业务判断后再 UPDATE,中间可能被篡改。
真正难的不是语法,而是把“锁失败”转化为稳定可测的业务路径 —— 比如重试次数、退避策略、对账补偿。这些不在 SQL 层解决。











