NOWAIT 使锁请求不等待而立即报错(错误码55P03),需应用层捕获并兜底;SKIP LOCKED 则跳过已锁行继续查询,适用于并发队列消费。

PostgreSQL 中 NOWAIT 怎么用,为什么一加就报错
加了 NOWAIT 却抛出 could not obtain lock on row,不是“不等待”吗?没错,它确实不等,但也不隐式失败——它会立刻报错,由你决定怎么兜底。常见于 SELECT ... FOR UPDATE 或 FOR SHARE 场景。
典型写法:
SELECT * FROM orders WHERE id = 123 FOR UPDATE NOWAIT;
如果该行正被其他事务锁定,这条语句不会挂起,而是直接返回错误。你需要在应用层捕获这个特定异常(PostgreSQL 错误码 55P03),再走降级逻辑,比如重试、跳过、或改查缓存。
-
NOWAIT只作用于当前语句申请的锁,不影响事务中后续语句 - 不能和
SKIP LOCKED同时用——二者语义冲突,PostgreSQL 会拒绝解析 - 某些 ORM(如 Django 的
select_for_update(nowait=True))底层就是翻译成这个语法,但要注意异常类型是否被正确映射
SKIP LOCKED 真实适用场景:队列消费与分片取数
它不是“跳过整张表”,而是跳过**当前扫描路径中已被其他事务加锁的行**,继续往后找可用行。最典型的是实现无竞争的 job 队列:
UPDATE tasks SET status = 'processing', worker_id = 'w1' WHERE id IN ( SELECT id FROM tasks WHERE status = 'pending' ORDER BY created_at LIMIT 10 FOR UPDATE SKIP LOCKED );
多个 worker 并发执行这段 SQL,不会互相阻塞,各自拿到互斥的 10 条任务。关键点:
-
SKIP LOCKED只在SELECT ... FOR UPDATE/SHARE中有效,不能单独用于UPDATE或DELETE - 必须配合
ORDER BY+LIMIT使用才有意义,否则可能跳过大量数据却只取到零星几条 - 在可重复读(RR)隔离级别下仍安全:它跳过的行对当前事务不可见,不会导致幻读问题
MySQL 的 SKIP LOCKED 和 PostgreSQL 有啥不一样
MySQL 8.0.1 才支持 SKIP LOCKED,行为基本一致,但有两个实际差异点:
- MySQL 不支持
NOWAIT(直到 8.0.29 才部分支持,且仅限SELECT ... FOR UPDATE,不支持FOR SHARE) - MySQL 的
SKIP LOCKED在二级索引覆盖扫描时可能跳过本应锁定的行(因锁粒度和 MVCC 实现差异),而 PostgreSQL 基于 tuple-level lock,更稳定 - MySQL 执行带
SKIP LOCKED的语句时,EXPLAIN显示的Extra字段会出现Using where; Skip locked,可用来确认是否生效
别以为加了就能高枕无忧:几个容易忽略的坑
SKIP LOCKED 和 NOWAIT 解决的是锁等待问题,但掩盖不了设计缺陷:
- 没加合适索引时,
FOR UPDATE SKIP LOCKED可能触发全表扫描+逐行加锁,性能断崖下跌——务必确保WHERE条件走索引 - 在长事务中反复用
SKIP LOCKED拿数据,可能因 MVCC 版本链过长导致查询变慢,甚至 OOM -
NOWAIT报错后不做重试或退避,直接返回失败,用户体验可能比短暂等待还差 - 有些数据库代理(如 ProxySQL、pgBouncer 连接池 in transaction pooling 模式)会吞掉
NOWAIT异常或干扰锁行为,务必在真实部署环境验证
真正难的从来不是语法,而是判断哪条数据值得锁、锁多久、谁来负责清理残留状态。










