NOWAIT 与 SKIP LOCKED 是什么
SELECT … FOR UPDATE 默认会一直等锁,高并发抢任务队列时会排出一条长长的等待队列。NOWAIT 让语句遇到已加锁的行就立刻报错返回;SKIP LOCKED 直接跳过被锁住的行,只返回能成功加锁的那些行。MySQL 8.0 与 PostgreSQL 9.5+ 都在 FOR UPDATE / FOR SHARE 上支持这两个修饰符,它们让「多个 worker 并发消费同一张任务表」变得简单而高效。
基本信息
| 适用平台 | MySQL 8.0+、PostgreSQL 9.5+ |
|---|---|
| 语法 | SELECT … FOR UPDATE NOWAIT / SELECT … FOR UPDATE SKIP LOCKED |
| NOWAIT 行为 | 遇到已锁行立即报错,由应用决定重试或放弃 |
| SKIP LOCKED 行为 | 跳过被锁行,只返回当前可锁的行 |
| 必须包事务 | 锁在事务提交或回滚时释放,不包事务等于没加锁 |
典型使用场景
- 任务队列取单:多个 worker 从 jobs 表抢任务,避免互相等待
- 消息表消费:每批取若干条待处理消息,处理完再改状态
- 分布式定时任务去重:同一时刻只允许一个实例拿到锁
- 批量清理数据:跳过正在被其他事务修改的行,避免长时间阻塞
- 库存扣减排队优化:抢不到就快速失败,由上层重试而不是干等
MySQL 抢任务写法
MySQL 抢任务写法
START TRANSACTION;
SELECT id, payload
FROM jobs
WHERE status = 'pending'
ORDER BY id
LIMIT 1
FOR UPDATE SKIP LOCKED; # 跳过已被其他 worker 锁住的任务
UPDATE jobs
SET status = 'running', worker = 'w1', started_at = NOW()
WHERE id = 5; # 用上一步拿到的 id
COMMIT; # 事务要尽量短,锁在这里释放PostgreSQL 用法
PostgreSQL 用法
BEGIN;
SELECT id FROM jobs
WHERE status = 'pending'
ORDER BY id
LIMIT 1
FOR UPDATE SKIP LOCKED; -- 取不到可锁行时返回空结果集
-- 快速失败:锁冲突时直接报错,不排队
SELECT id FROM jobs WHERE status = 'pending' FOR UPDATE NOWAIT;
COMMIT;三种锁读方式对照
| 写法 | 遇到已锁行 | 适用判断 |
|---|---|---|
| FOR UPDATE | 一直等待直到锁超时 | 必须处理这一行、且可以等 |
| FOR UPDATE NOWAIT | 立即报错返回 | 快速失败、由应用重试 |
| FOR UPDATE SKIP LOCKED | 跳过该行继续找下一行 | 任务队列、并发消费 |
| FOR SHARE SKIP LOCKED | 跳过被排他锁占用的行 | 读多写少的共享校验场景 |
SKIP LOCKED 的结果是不确定的:被跳过的行可能进本轮也可能进下一轮,绝不能用于统计、对账或任何需要完整数据的查询,否则会出现「结果少了几行但没有任何报错」。
NOWAIT 抛出的是锁冲突错误而不是业务异常,应用必须能区分二者(MySQL 错误码 3572,PostgreSQL 的 SQLSTATE 55P03),否则会把正常的锁竞争误记成任务失败。