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

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 UPDATE 或 FOR SHARE 的行”,对 INSERT ... SELECT 或显式 LOCK TABLE 不生效
- 在 MySQL 中,若隔离级别是
READ COMMITTED,SKIP LOCKED 行为与 PostgreSQL 略有差异:MySQL 可能因 MVCC 快照导致“跳过本应可见的行”,建议统一用 REPEATABLE READ
- 没有
ORDER BY 时,数据库可能每次返回不同子集,造成某些行长期“被跳过”,看起来像饥饿(starvation)
- 如果
WHERE 条件匹配 0 行,查询返回空结果,不会报错,应用层要主动判断是否需要重试或退出
替代方案对比(没 SKIP LOCKED 怎么办)
SKIP LOCKED 只跳过“当前已被其他事务加了 FOR UPDATE 或 FOR SHARE 的行”,对 INSERT ... SELECT 或显式 LOCK TABLE 不生效 READ COMMITTED,SKIP 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 是目前最简洁、原子性最强的原生方案,前提是你的数据库版本和隔离级别支持它。别忘了检查执行计划里是否真的用了索引——否则锁表一秒钟,整个队列就卡住了。










