SKIP LOCKED本身不构成消息队列,仅支持多消费者并发抢任务而不阻塞;需满足显式事务、索引覆盖、原子更新三条件,否则仍会超卖、空跑或排队。

SKIP LOCKED 本身不构成消息队列,它只是让多个消费者能并发“抢”到未处理的任务行而不互相阻塞——能不能用好,全看你怎么建表、写 SQL、控事务。
为什么传统 SELECT FOR UPDATE 在队列表里会卡死
假设你有个 jobs 表,状态字段 status 标识任务是否待处理。老写法是:
BEGIN; SELECT * FROM jobs WHERE status = 'pending' LIMIT 1 FOR UPDATE; -- 处理完再 UPDATE status = 'processing' COMMIT;
问题就出在:线程 A 锁住第一行后,线程 B 执行同样语句,会直接卡在 FOR UPDATE 上等锁释放。哪怕你起 100 个 worker,实际仍是串行取任务。
根本原因不是数据库慢,而是锁住了“不该锁的范围”或“没跳过已锁行”。
真正能并发取任务的 SKIP LOCKED 写法
必须满足三个硬条件:显式事务 + 索引覆盖 + 原子更新。典型结构如下:
BEGIN; SELECT id, payload FROM jobs WHERE status = 'pending' ORDER BY id ASC LIMIT 1 FOR UPDATE SKIP LOCKED; -- 拿到 id 后立刻: UPDATE jobs SET status = 'processing' WHERE id = ? AND status = 'pending'; COMMIT;
关键点:
-
ORDER BY字段(如id)必须有索引,否则 MySQL 可能全表扫描,SKIP LOCKED只能跳“已锁行”,跳不掉“本不该扫到的行” - UPDATE 必须带
status = 'pending'双重校验,防止 SELECT 和 UPDATE 之间被其他事务抢先改掉状态 - 不能把业务逻辑(比如调第三方 API)塞进事务里,锁持有时间越短,并发吞吐越高
- 如果
SELECT返回空,说明当前无待处理任务,直接COMMIT退出,不要SLEEP或重试
哪些地方一踩就超卖或空跑
SKIP LOCKED 是个精细活,失效往往不出现在语法上,而出现在边界控制里:
- AUTOCOMMIT=1 时,
SELECT ... FOR UPDATE SKIP LOCKED执行完锁立刻释放,后续UPDATE是新事务,别人早已插队 - 隔离级别设成
SERIALIZABLE,MySQL 会直接报错ER_NOT_SUPPORTED_YET;设成READ UNCOMMITTED则压根不支持SKIP LOCKED - WHERE 条件里用了函数,比如
WHERE DATE(created_at) = '2026-06-10',索引失效,退化为全表扫描+大量行锁 - 用
IN (SELECT ...)子查询套FOR UPDATE SKIP LOCKED,MySQL 直接报错,不支持嵌套 - 所有任务都挤在同一个热点行上(比如单条“发送欢迎邮件”任务),
SKIP LOCKED没别的行可跳,还是排队
它适合什么,不适合什么
别把它当万能药。真实瓶颈常藏在别处:
- 适合:事务强一致场景,比如订单创建后必须同步更新积分,且日任务量在百万级以内
- 不适合:需要延迟执行、失败自动重试、跨服务事务补偿的复杂流程——这些得靠 RabbitMQ 或 Kafka
- 容易被忽略的是:
SKIP LOCKED的有效性完全依赖事务边界的紧致性。一旦 SELECT 和 UPDATE 中间掺了网络 IO、日志打印、甚至一个sleep(100),它就退化成语法装饰品



















