nowait不是跳过锁等待,而是彻底取消等待、立即报错——select ... for update nowait在无法获取行锁时秒级失败,返回error 3572,不占用连接、不挂起线程,且仅作用于行级记录锁,要求mysql 8.0.1+、显式事务、紧贴for update/share使用,不支持子查询或dml语句。

直接说结论:NOWAIT 不是“跳过锁等待”,而是“不等、立刻报错”——它让 SELECT ... FOR UPDATE 在拿不到行锁时秒级失败,返回 ERROR 3572,而不是卡住等 innodb_lock_wait_timeout 超时。
为什么 SELECT ... FOR UPDATE NOWAIT 会立刻报错而不是卡住
它不是把等待时间调短,而是彻底取消等待逻辑。只要目标行(或扫描过程中碰到的任意一行)已被其他事务加了 FOR UPDATE 或 FOR SHARE 锁,当前语句就终止执行,抛出固定错误:ERROR 3572 (HY000): Statement aborted because lock(s) could not be acquired immediately and NOWAIT is set。
这和调小 innodb_lock_wait_timeout 有本质区别:后者仍是阻塞,只是等得短;NOWAIT 是非阻塞设计,连接不占用、线程不挂起。
- 错误码必须精确捕获:
SQLSTATE = 'HY000'且errno = 3572,不能只抓1205(那是超时)或泛化处理所有 SQLException - 它只作用于行级记录锁(record lock),对间隙锁(gap lock)或 next-key 锁无效——若因 gap lock 导致无法插入/更新,NOWAIT 不起作用
- DDL 也有
NO_WAIT(注意大小写和拼写),但那是针对 MDL 锁的,和这里的NOWAIT完全无关,混用会语法报错
NOWAIT 的硬性生效条件
它不是加了就生效,必须同时满足以下全部条件:
-
SELECT语句中必须显式包含FOR UPDATE或FOR SHARE,且NOWAIT必须紧贴其后(不能换行、不能有注释隔开) - 必须在显式事务中执行:
START TRANSACTION或SET autocommit = 0;若autocommit = 1,语句会隐式开启并立即提交事务,锁无法延续到后续操作 - 仅 MySQL 8.0.1+ 支持;低版本直接报
Unknown syntax - 不支持子查询、
UNION、视图、UPDATE或DELETE语句——例如UPDATE t SET x=1 NOWAIT语法错误,SELECT * FROM t WHERE id IN (SELECT id FROM t2 FOR UPDATE NOWAIT)中的 NOWAIT 会被忽略
报错 ERROR 3572 后该怎么做
这不是数据库异常,而是你主动设计的“快速失败路径”被触发了。应用层必须专门响应,而不是当作崩溃处理。
- 典型 fallback 策略包括:
SKIP LOCKED再查一次、返回“资源已被占用”提示、将请求写入异步队列重试、或走降级逻辑(如跳过扣减、标记待人工审核) - 切忌无限制重试原语句——若没做限流或退避(如指数退避),高并发下可能瞬间打满连接池
- 若用 ORM(如 MyBatis、Hibernate),需确认是否透传原生错误码——部分框架会把
3572包装成通用SQLException,丢失语义
为什么加了 NOWAIT 还是报错?先看执行计划
NOWAIT 失败 ≠ “没查到数据”,极可能是锁落在了**非目标行但被扫描到的索引记录上**。比如 WHERE 条件没走索引,触发全表扫描,就会对所有扫描行加意向锁;哪怕你只想要 id = 100,也可能因扫了 1 万行而被其中任意一行的锁挡住。
实操建议:
- 用
EXPLAIN SELECT ... FOR UPDATE NOWAIT确认是否走了索引,警惕type: ALL或Extra: Using where - 确保
WHERE条件列上有合适索引(主键、唯一键、覆盖索引优先) - 避免在高并发热点行场景下依赖 NOWAIT 单独兜底——它解决的是“等不等”的问题,不是“能不能抢到”的问题;业务层仍需幂等、限流、异步补偿等配套机制











