SKIP_LOCKED 是 MySQL 8.0 引入的 SELECT ... FOR UPDATE 修饰符,使事务跳过已被锁的行而非阻塞等待,解决抢购中重复扣减与阻塞问题;需配合事务、索引及原子化“查即锁+更新”逻辑。

SKIP_LOCKED 是什么,为什么抢购场景必须用它
MySQL 8.0 引入的 SKIP_LOCKED 是 SELECT ... FOR UPDATE 的修饰符,作用很直接:当多事务并发执行带锁查询时,它让当前事务跳过已被其他事务加锁的行,而不是阻塞等待。在抢购或任务队列这类“取一条、处理一条、不能重复”的场景里,不用它就容易卡住或漏单——比如两个消费者同时 SELECT ... FOR UPDATE LIMIT 1,其中一个会等另一个释放锁,另一个处理完提交后,前者才查到同一行,结果双花。
它不是银弹,但确实是解决“悲观锁竞争”最轻量的原生方案。
怎么写一个带 SKIP_LOCKED 的抢购扣减 SQL
核心是把传统“查再锁”改成“查即锁且跳过已锁”,且必须搭配事务和合适的索引:
- 确保库存字段(如
stock)有索引,否则ORDER BY+LIMIT可能全表扫描,锁范围失控 - SQL 必须显式开启事务,
AUTOCOMMIT=0,否则锁立刻释放 - 典型写法:
BEGIN; SELECT id, stock FROM items WHERE status = 'on_sale' AND stock > 0 ORDER BY updated_at ASC LIMIT 1 FOR UPDATE SKIP_LOCKED;
- 拿到结果后,立刻执行
UPDATE items SET stock = stock - 1 WHERE id = ?,再COMMIT - 如果
SELECT返回空,说明没可用库存,无需重试或等待
常见翻车点:为什么加了 SKIP_LOCKED 还会重复扣减
这不是 SKIP_LOCKED 的问题,而是逻辑没兜住:
- 没在同一个事务里完成「查询+更新」:查完提交了,再 update 就是新事务,锁已释放,别人可能插队
- WHERE 条件没覆盖业务约束:比如漏了
status = 'on_sale',下架商品也可能被捞出来 - ORDER BY 字段无索引或存在大量重复值:MySQL 可能选中不同行,导致多个事务“各取各的”,但实际业务要求严格 FIFO 或唯一性
- 应用层没检查 SELECT 结果是否为空:直接拿
NULL去 update,可能误操作默认值或触发意外逻辑 - 用了
READ COMMITTED隔离级别但没注意:该级别下,锁只在当前语句生效,必须靠事务包住整套动作
和乐观锁比,什么时候该选 SKIP_LOCKED
它适合“冲突高频但处理快”的场景,比如秒杀下单、消息队列分发;不适合长事务或需要复杂校验的流程:
- 优势明显:无版本号列、无重试逻辑、不依赖应用层判断冲突,数据库层直接过滤
- 劣势也很实在:只能用于
SELECT ... FOR UPDATE/SHARE,无法用于 INSERT 或 DELETE 场景;也不支持 MyISAM 或低版本 MySQL - 若你的任务要先查再调外部 API 再更新,耗时超过几百毫秒,SKIP_LOCKED 反而会拉长锁持有时间,这时更适合结合
INSERT ... ON DUPLICATE KEY UPDATE或 Redis 分布式锁 - 注意:MySQL 8.0.22+ 才支持
SKIP_LOCKED在SELECT ... FOR SHARE中使用,早期版本仅限FOR UPDATE
实际压测中,500 并发抢购时,用 SKIP_LOCKED 的吞吐通常比乐观锁重试高 30%–50%,但前提是你的 WHERE 条件足够高效、索引能精准定位——这点容易被忽略,一上来就堆并发,结果慢在磁盘 IO 上,不是锁上。











