SKIP LOCKED 是让多事务不阻塞选行的开关,仅在 SELECT+UPDATE 同事务且索引/写法完全匹配时生效;否则退化为普通 FOR UPDATE,甚至引发隐蔽锁竞争。

SKIP LOCKED 不是并发更新的“加速器”,而是让多个事务从同一张表里**不互相阻塞地选行**的开关——它只在 SELECT + UPDATE 绑定在同一事务、且索引和写法完全匹配时才真正起效。写错任意一环,它就退化成普通 FOR UPDATE,甚至引发更隐蔽的锁竞争。
为什么写了 SKIP LOCKED 还卡在同一行?
根本不是语法错了,而是查询没落到“可跳过的行集合”里:
-
WHERE status = 'PENDING'没走索引 → 全表扫描 → MySQL 尝试对几百行加锁,SKIP LOCKED只能跳过其中已被锁的几行,但大量未锁行仍被无谓扫描并短暂加锁,后续事务反复撞上同一页物理行 -
ORDER BY id字段无索引 → MySQL 无法稳定定位下一条可用行,多个消费者可能反复选中同一批id(尤其主键有空洞或批量删除后) - 事务未显式开启:
AUTOCOMMIT = 1下,SELECT ... FOR UPDATE SKIP LOCKED执行完锁立刻释放,紧接着的UPDATE是新事务,别人早已插队 - 隔离级别为
SERIALIZABLE→ MySQL 直接报错ER_NOT_SUPPORTED_YET,SKIP LOCKED被禁用
怎么写出真正不抢同一行的队列 SQL?
必须把“查、锁、判、更”压进一个事务,并确保索引覆盖全部过滤和排序路径:
面向 CRM 系统的 MySQL 8 数据库模式设计。当用户需要为客户关系管理(CRM)系统设计、评审、优化或生成数据库模式时,应使用此项技能。
- 建联合索引时顺序关键:
CREATE INDEX idx_status_id ON jobs (status, id)——status在前才能高效过滤,id在后才支撑ORDER BY id;status单独索引不够,id单独索引也无效 - 事务第一句必须是带
SKIP LOCKED的SELECT,且只查id和必要字段,避免锁升级成间隙锁或表锁 - 拿到结果后立刻检查
id是否为空;若为空,说明无可用任务,直接COMMIT退出,不要SLEEP或重试 -
UPDATE必须用主键精确匹配 + 业务条件:UPDATE jobs SET status = 'PROCESSING' WHERE id = ? AND status = 'PENDING'—— 后置校验防 ABA,哪怕SELECT时状态是PENDING,UPDATE前也可能被别人改掉
哪些场景下 SKIP LOCKED 根本不起作用?
它解决不了单点热行竞争,只对“多行可选”的队列有效:
- 所有消费者都在抢同一张优惠券的总库存(单行
coupon_id = 1)→ 没别的行可跳,大家还是卡在这一行上等待 -
WHERE中用了函数或隐式转换,比如WHERE DATE(created_at) = '2026-06-15'或WHERE status = 0(字段是字符串)→ 索引失效,退化为全表扫描 + 大量无效加锁 - 子查询中使用:
UPDATE tasks SET done = 1 WHERE id IN (SELECT id FROM jobs FOR UPDATE SKIP LOCKED)→ 直接报错ER_NOT_SUPPORTED_YET - Spring 的
@Transactional把整个方法包进事务,中间夹着远程调用或耗时逻辑 → 锁持有时间过长,实际变成串行执行
SKIP LOCKED 的核心约束很硬:它只保护“领取那一刻”,不负责后续失败恢复。一旦 worker 领取后崩溃,任务停在 PROCESSING 状态,就再没人能处理它——这个漏洞比锁争抢更难排查。

















