SELECT ... FOR UPDATE SKIP LOCKED 是唯一能真正解决高并发抢锁安全性的子查询手段,因其直接在主查询层实时跳过已锁行;普通子查询加 FOR UPDATE 无法跳过锁,反而因物化导致阻塞或死锁。

SELECT ... FOR UPDATE SKIP LOCKED 是唯一能真正解决高并发抢锁安全性的子查询手段。其他所谓“子查询绕过锁”的写法,比如 SELECT id FROM (SELECT * FROM task WHERE status = 'pending') AS t LIMIT 1 再套 FOR UPDATE,根本不会跳过已锁行,反而可能因子查询物化导致更严重的阻塞或死锁。
为什么不能用普通子查询加 FOR UPDATE 实现抢锁
PostgreSQL 的 FOR UPDATE 只作用于最外层查询命中的行,子查询本身不持有锁,也不参与锁跳过逻辑。常见错误是以为嵌套一层就能“隔离”竞争,实际效果等于没加 SKIP LOCKED:
- 子查询结果集在执行时就被物化(尤其带
LIMIT或ORDER BY),后续FOR UPDATE仍会逐行尝试加锁,遇到已被锁的行就阻塞 - 如果子查询用了窗口函数、聚合或 CTE,还可能触发多次扫描,放大锁冲突概率
-
WITH ... AS materialized_cte显式物化后加FOR UPDATE,等价于对临时结果集加锁——但该结果集早就不反映实时状态了
正确写法:SKIP LOCKED 必须直接挂在主 SELECT 上
抢锁逻辑必须扁平、原子、无中间物化。所有过滤、排序、分页都应在同一层完成,SKIP LOCKED 才能实时感知行锁状态:
SELECT id, payload FROM task WHERE status = 'pending' ORDER BY created_at LIMIT 100 FOR UPDATE SKIP LOCKED;
-
SKIP LOCKED只支持FOR UPDATE和FOR NO KEY UPDATE,不支持FOR SHARE - 必须搭配显式事务:
BEGIN→ 查询 →UPDATE ... SET status = 'running' WHERE id IN (...)→COMMIT,否则锁释放太晚 - 如果业务需要按权重或随机取样,可用
ORDER BY random(),但注意性能开销;避免OFFSET,它会强制扫描前 N 行并尝试加锁
容易被忽略的兼容性与行为细节
SKIP LOCKED 看似简单,但版本差异和执行计划会悄悄破坏语义:
- PostgreSQL 9.5+ 才支持,9.4 及更早版本用
NOWAIT+ 重试是唯一选择,但失败率高、CPU 毛刺明显 - 如果查询走索引扫描,
SKIP LOCKED能高效跳过;若退化为顺序扫描,会逐行检查锁状态,吞吐骤降 - 复合条件如
WHERE status = 'pending' AND worker_id IS NULL,必须确保有覆盖索引(如INDEX ON task (status, worker_id) WHERE status = 'pending'),否则SKIP LOCKED效率归零 - 不要在同一个事务里多次执行带
SKIP LOCKED的查询——第二次可能拿到空结果,因为第一次已把所有可锁行锁住且未更新状态
真正的难点不在语法,而在于让每一行“被跳过”和“被选中”都发生在毫秒级的锁状态快照里;任何物化、缓存、二次筛选都会让这个快照失真。

















