SQL 如何用 SELECT … FOR UPDATE SKIP LOCKED 避免阻塞

1次阅读

selectfor UPDATE SKIP LOCKED 是 postgresqlmysql 8.0+ 支持的语句修饰符,用于加排他锁时跳过已被锁定的行,实现无阻塞并发任务分发,避免锁等待,适用于任务队列、分布式抢购等场景。

SQL 如何用 SELECT … FOR UPDATE SKIP LOCKED 避免阻塞

SELECT … FOR UPDATE SKIP LOCKED 是什么

SELECT ... FOR UPDATE SKIP LOCKED

是 PostgreSQL 和 MySQL 8.0+ 支持的语句修饰符,用于在读取行的同时加排他锁(X锁),但跳过那些已被其他事务锁定的行——而不是等待它们释放锁。它本质是“乐观抢占式加锁”,适合任务队列、分布式抢购、并发消费等场景。

不加 SKIP LOCKED 时,SELECT ... FOR UPDATE 遇到已锁行会阻塞,直到锁释放或超时;加了之后,直接过滤掉这些行,返回剩余可锁的行。

注意:sqlite、SQL Server、oracle 原生不支持该语法(Oracle 有类似 SELECT ... FOR UPDATE NOWaiT + 应用层重试,但逻辑不同)。

典型使用场景和写法

最常见用途是实现“无竞争的任务分发”:多个工作进程并发执行 SELECT ... FOR UPDATE SKIP LOCKED,各自拿到一批未被处理、也未被其他进程锁定的记录,然后更新状态并处理。

例如从待处理订单表中取 5 条:

SELECT id, order_no FROM orders  WHERE status = 'pending'  ORDER BY created_at  LIMIT 5  FOR UPDATE SKIP LOCKED;

关键点:

  • WHERE 条件必须能走索引,否则全表扫描 + 锁表风险高
  • ORDER BY 推荐配合索引字段,避免排序开销影响并发吞吐
  • LIMIT 要有,否则可能锁住大量无关行
  • 执行后需尽快做 UPDATE(比如把 status 改为 'processing'),否则锁持有时间过长,反而降低并发

容易踩的坑

  • SKIP LOCKED 只跳过“当前已被其他事务加了 FOR UPDATEFOR SHARE 的行”,对 INSERT ... SELECT 或显式 LOCK table 不生效
  • 在 MySQL 中,若隔离级别是 READ COMMITTEDSKIP LOCKED 行为与 PostgreSQL 略有差异:MySQL 可能因 MVCC 快照导致“跳过本应可见的行”,建议统一用 REPEATABLE READ
  • 没有 ORDER BY 时,数据库可能每次返回不同子集,造成某些行长期“被跳过”,看起来像饥饿(starvation)
  • 如果 WHERE 条件匹配 0 行,查询返回空结果,不会报错,应用层要主动判断是否需要重试或退出

替代方案对比(没 SKIP LOCKED 怎么办)

老系统或兼容性要求高的环境,常靠组合手段模拟:

  • SELECT ... FOR UPDATE NOWAIT + 捕获锁冲突异常(如 PostgreSQL 的 55P03,MySQL 的 1205),再重试
  • 加一个 worker_id 字段,用 UPDATE ... WHERE status = 'pending' LIMIT 1 RETURNING *(PostgreSQL)原子抢锁
  • 引入 redis 分布式锁预筛选,再查 DB,但增加系统复杂度和一致性风险

SKIP LOCKED 是目前最简洁、原子性最强的原生方案,前提是你的数据库版本和隔离级别支持它。别忘了检查执行计划里是否真的用了索引——否则锁表一秒钟,整个队列就卡住了。

text=ZqhQzanResources