skip_locked不是自动跳过锁的开关,而是需配合事务、索引和原子更新的行级调度机制;传统select for update不加skip_locked会导致多事务阻塞同一行,使任务队列串行化,吞吐量降至单线程水平。

SKIP_LOCKED 不是“自动跳过锁”的开关,而是必须配合事务、索引和原子更新逻辑才能生效的行级调度机制。 它不能单独解决高并发排队问题,但能让你的消费者线程真正并行起来——前提是 SQL 写法和事务边界没踩坑。
为什么 SELECT FOR UPDATE 不加 SKIP_LOCKED 会导致任务队列串行化
传统写法 SELECT * FROM jobs WHERE status = 'PENDING' LIMIT 1 FOR UPDATE 在高并发下会卡死:多个事务同时执行这条语句,InnoDB 按索引顺序扫描,第一个事务锁住第一行后,其余事务全部阻塞在该行上等待释放。这不是性能问题,是逻辑阻塞——所有消费者被迫排队,吞吐量被压到单线程水平。
常见错误现象包括:
- 数据库连接池持续增长,
SHOW PROCESSLIST中大量线程处于Locked或Waiting for table metadata lock状态 - 业务侧超时频发,但实际没多少任务被处理
- 监控看到 CPU 和 IOPS 并不高,但 QPS 卡在几十甚至个位数
正确写法:事务内完成“查即锁 + 原子更新”
核心不是“跳过锁”,而是让每个事务拿到一个**互不重叠的可用行**,且后续操作不破坏这个前提。典型可靠流程如下:
START TRANSACTION; SELECT id, status FROM jobs WHERE status = 'PENDING' ORDER BY id LIMIT 1 FOR UPDATE SKIP LOCKED; -- 拿到 id 后立即执行: UPDATE jobs SET status = 'PROCESSING', updated_at = NOW() WHERE id = ? AND status = 'PENDING'; -- 检查 affected rows:为 0 表示该行已被其他事务抢先更新,直接 COMMIT 并退出(不重试) COMMIT;
关键点:
-
ORDER BY字段必须有索引,否则LIMIT 1可能选中任意行,SKIP LOCKED失效 -
UPDATE必须带原始业务条件(如status = 'PENDING'),防止 ABA 场景下误更新已变更状态的记录 - 不能把
SELECT和UPDATE拆到两个事务里;也不能在 Spring@Transactional方法里先查再调另一个 service 更新——那会跨事务丢锁 - 隔离级别建议用
REPEATABLE READ或READ COMMITTED,但后者需注意锁只在语句级有效,必须确保整套操作在一个事务内
容易被忽略的三个硬性依赖
SKIP_LOCKED 不是语法糖,它对底层结构有明确要求:
- 表必须使用
InnoDB引擎;MyISAM或内存表不支持 -
WHERE条件必须能走索引,否则全表扫描时每行都尝试加锁,SKIP LOCKED退化为“跳过所有行”,最终SELECT返回空,业务误判为队列为空 - 不能在子查询中使用,例如
UPDATE ... WHERE id IN (SELECT id FROM jobs FOR UPDATE SKIP LOCKED)会报错ER_NOT_SUPPORTED_YET
尤其要注意:如果你的 jobs 表用的是复合状态字段(比如 status TINYINT),但没给 status 单独建索引,WHERE status = 'PENDING' 就可能无法高效定位,导致 SKIP_LOCKED 实际跳过大量无效行后仍无结果。
和 NOWAIT、乐观锁对比时的真实定位
SKIP_LOCKED 的价值不在“替代”其他方案,而在“补位”:
-
NOWAIT是失败快抛错,适合你希望立刻知道“没资源可取”的场景,但它不帮你找下一条;SKIP_LOCKED是主动绕开、继续找,适合队列类长尾消费 - 乐观锁(如
version字段)需要应用层重试,而SKIP_LOCKED把冲突过滤下推到数据库层,减少网络往返和事务回滚开销 - 但它扛不住超长事务:如果某个消费者卡住 30 秒不提交,它锁住的那行就会长期不可见,其他线程只能往后扫——所以业务上仍需设置合理的
innodb_lock_wait_timeout和超时兜底逻辑
最常被低估的一点:它只在“取单条、处理快、失败少”的场景下收益最大。一旦你的任务平均耗时超过 500ms,或者失败率高于 5%,光靠 SKIP_LOCKED 就很难维持稳定吞吐——这时候得结合死信队列或重试策略,而不是指望 SQL 层解决所有问题。











