SKIP LOCKED 仅在SELECT...FOR UPDATE SKIP LOCKED与后续UPDATE同属一事务、且索引覆盖WHERE和ORDER BY条件时生效;否则仍会锁多行、争抢物理位置或退化为间隙锁。

SKIP LOCKED 不是吞吐量加速器,而是避免事务排队的开关
它只在 SELECT ... FOR UPDATE SKIP LOCKED 和后续 UPDATE 绑定在同一事务、且索引覆盖全部过滤与排序条件时才真正起效。否则你看到的“不卡”,只是表象——背后可能在锁几百行、反复争抢同一物理位置、甚至退化成间隙锁阻塞插入。
为什么任务队列用了 SKIP LOCKED 还卡在一行上?
根本不是语法写错了,而是查询没落到“可跳过的行集合”里:
-
WHERE status = 'PENDING'没走索引 → 全表扫描 → 锁住大量无关行,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'→ 索引失效,退化为全表扫描 + 大量无效加锁 - 子查询中使用:
UPDATE tasks SET done = 1 WHERE id IN (SELECT id FROM jobs FOR UPDATE SKIP LOCKED)→ 直接报错ER_NOT_SUPPORTED_YET - Spring 的
@Transactional把整个方法包进事务,中间夹着远程调用或耗时逻辑 → 锁持有时间拉长,实际并发度归零
最易被忽略的一点:SKIP LOCKED 的效果高度依赖执行计划是否走索引。哪怕建了联合索引,如果 WHERE 条件顺序不对、或用了 OR、LIKE '%xxx'、或字段类型与参数不一致导致隐式转换,都会让索引失效——这时你写的每条 SKIP LOCKED 都在假装高效。

















