秒杀场景下SELECT ... FOR UPDATE会卡住,因默认阻塞等待锁释放或超时(50秒),导致请求堆积、延迟飙升;MySQL 8.0需用NOWAIT(立即报错ERROR 3572)或SKIP LOCKED(跳过已锁行)实现非阻塞锁定读,二者仅作用于行锁,须显式开启事务且配合主键精确查询或ORDER BY + LIMIT使用。

秒杀场景下SELECT ... FOR UPDATE为什么会卡住?
MySQL 8.0 默认的 SELECT ... FOR UPDATE 在行被锁时会阻塞等待,直到锁释放或超时(默认 50 秒),这在秒杀中直接导致请求堆积、响应延迟飙升,甚至引发雪崩。这不是业务逻辑慢,是锁机制本身在等。
关键点在于:锁等待不是“没查到数据”,而是“查到了,但别人正拿着锁改它”。此时你看到的典型现象是慢查询日志里大量 Waiting for table metadata lock 或 Waiting for a row lock,且 SHOW PROCESSLIST 中状态长期为 updating 或 Locked。
解决思路不是加索引或拆表,而是让数据库明确知道:“我要锁,但不等”。这就需要 NOWAIT 或 SKIP LOCKED —— 它们不是锦上添花,是秒杀链路里的必选项。
NOWAIT:抢不到就立刻失败,适合强一致性校验
NOWAIT 的作用是让 SELECT ... FOR UPDATE 在遇到锁时**不等待,直接报错**。这对库存扣减前的“预占”非常关键:避免用户长时间卡在排队页,也防止事务堆积拖垮连接池。
使用方式很简单,加在语句末尾即可:
SELECT stock FROM items WHERE id = 123 FOR UPDATE NOWAIT;
一旦行被其他事务锁定,MySQL 立即返回错误:ERROR 1205 (40001): Deadlock found when trying to get lock; try restarting transaction(注意:实际错误码是 1205,不是超时类错误)。
你需要在应用层捕获这个错误,并返回“商品已被抢光”或“请重试”。不能忽略它,也不能当成普通异常兜底重试——那会放大竞争。
- 只适用于单行精确查询(
WHERE id = ?),范围查询用NOWAIT可能锁住多行,失败概率更高 -
NOWAIT不改变锁粒度,仍是行锁;它只改变等待行为 - 与
innodb_lock_wait_timeout无关——设再小也没用,NOWAIT优先级更高
SKIP LOCKED:从可用库存里“跳过已锁行”,适合多库存分片场景
当库存分散在多条记录上(比如按仓库、分库分表后的多个 items 行),用 NOWAIT 会频繁失败。这时 SKIP LOCKED 更合适:它让查询**自动跳过当前被锁的行,继续找下一个可用的**。
典型用法:
SELECT id, stock FROM items WHERE status = 'on_sale' ORDER BY id LIMIT 1 FOR UPDATE SKIP LOCKED;
这条语句会扫描满足条件的行,跳过所有已被其他事务加锁的行,只返回第一个未被锁的。哪怕前 10 行全锁了,它也会继续往后找,直到找到或扫完。
注意:SKIP LOCKED 要求必须有明确的排序(ORDER BY)和限制(LIMIT),否则可能返回意外结果;它也不保证“全局最小 ID”,只保证本次扫描中第一个未锁的。
-
SKIP LOCKED是 MySQL 8.0.1 引入的,低版本不可用 - 不能和
NOWAIT同时用——语法冲突,MySQL 报错ERROR 1064 - 如果所有匹配行都被锁,查询返回空结果集(不是报错),应用需判断并提示“暂无可用库存”
为什么不能只靠应用层排队?
很多人试图用 Redis 队列 + MySQL 扣减来解耦,但漏掉一个关键点:Redis 队列只是“请求顺序”,而 MySQL 的行锁才是“执行顺序”。如果 100 个请求从 Redis 出来后同时打到同一行,它们仍会在 MySQL 层排队——只不过把等待从应用移到了数据库,本质没变。
真正有效的组合是:Redis 预减库存(原子操作) + MySQL 行锁(NOWAIT/SKIP LOCKED)做最终落库校验。前者挡掉 99% 的无效请求,后者确保 DB 层不出现超卖。
容易被忽略的是隔离级别:READ COMMITTED 下 SKIP LOCKED 行为更可预测;若用 REPEATABLE READ(默认),MVCC 可能影响可见性判断,建议显式设置 SET TRANSACTION ISOLATION LEVEL READ COMMITTED。


















