如何用 NOWAIT / SKIP LOCKED 避免阻塞等待

8次阅读

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

如何用 NOWAIT / SKIP LOCKED 避免阻塞等待

postgresqlNOWAIT 怎么用,为什么一加就报错

加了 NOWAIT 却抛出 could not obtain lock on row,不是“不等待”吗?没错,它确实不等,但也不隐式失败——它会立刻报错,由你决定怎么兜底。常见于 select ... for UPDATEFOR SHARE 场景。

典型写法:

SELECT * FROM orders WHERE id = 123 FOR UPDATE NOWAIT;

如果该行正被其他事务锁定,这条语句不会挂起,而是直接返回错误。你需要在应用层捕获这个特定异常(PostgreSQL 错误码 55P03),再走降级逻辑,比如重试、跳过、或改查缓存。

  • NOWAIT 只作用于当前语句申请的锁,不影响事务中后续语句
  • 不能和 SKIP LOCKED 同时用——二者语义冲突,PostgreSQL 会拒绝解析
  • 某些 ORM(如 Djangoselect_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 中有效,不能单独用于 UPDATEdelete
  • 必须配合 ORDER BY + LIMIT 使用才有意义,否则可能跳过大量数据却只取到零星几条
  • 在可重复读(RR)隔离级别下仍安全:它跳过的行对当前事务不可见,不会导致幻读问题

mysqlSKIP 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 LOCKEDNOWAIT 解决的是锁等待问题,但掩盖不了设计缺陷:

  • 没加合适索引时,FOR UPDATE SKIP LOCKED 可能触发全表扫描+逐行加锁,性能断崖下跌——务必确保 WHERE 条件走索引
  • 在长事务中反复用 SKIP LOCKED 拿数据,可能因 MVCC 版本链过长导致查询变慢,甚至 OOM
  • NOWAIT 报错后不做重试或退避,直接返回失败,用户体验可能比短暂等待还差
  • 有些数据库代理(如 proxySQL、pgBouncer 连接池 in transaction pooling 模式)会吞掉 NOWAIT 异常或干扰锁行为,务必在真实部署环境验证

真正难的从来不是语法,而是判断哪条数据值得锁、锁多久、谁来负责清理残留状态。

text=ZqhQzanResources