# 锁与并发 · 咨询锁与 SKIP LOCKED

> 咨询锁(advisory lock)是 PostgreSQL 提供的「应用层的锁」，纯靠整数键标识、不绑任何行，让 PG 直接当分布式锁用；而 SELECT ... FOR UPDATE SKIP LOCKED 跳过已被锁的行，是「用数据库当任务队列」的关键——N 个 worker 各领各的活，不打架、不用 Redis。

- 作者：David（道雾轩）
- 专栏：PostgreSQL 特性详解（https://daiw.org/manual/postgresql.md）
- 最后更新：2026-07-13
- 原文：https://daiw.org/manual/postgresql/locks
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

# 锁与并发 · 咨询锁与 SKIP LOCKED

并发场景里有两类经典需求：「**同一件事，只允许一个进程在干**」（防重入、选主），和「**一堆任务，多个 worker 分着做、不重复领**」（任务队列）。很多人第一反应是上 Redis。但 PostgreSQL 自带两把锁——**咨询锁**和**行级锁 + `SKIP LOCKED`**——把这两类需求都在数据库内解决了。

## 咨询锁：把 PostgreSQL 当分布式锁

咨询锁(Advisory Lock)是 PostgreSQL 里一种特别的锁：它**不绑任何具体的行或表**，纯粹靠一个（或两个）整数键来标识，含义完全由应用约定。数据库只负责「这个键当前有没有被别人占着」。

```sql
-- 事务级:非阻塞尝试拿键为 42 的锁
SELECT pg_try_advisory_xact_lock(42);   -- -> true(拿到了)
SELECT pg_try_advisory_xact_lock(42);   -- 同事务内可重入 -> true

-- 双键版本(常用于「资源类型 + 资源 ID」)
SELECT pg_advisory_xact_lock(1001, 99); -- -> 拿到

-- 查 pg_locks 看自己持有的 advisory 锁
-- ('advisory', 0, 42, 'ExclusiveLock')
-- ('advisory', 1001, 99, 'ExclusiveLock')
```

一组函数覆盖不同用法：

| 函数 | 行为 |
| --- | --- |
| `pg_advisory_lock(key)` | 阻塞获取，**事务结束不自动释放**，要手动 unlock |
| `pg_try_advisory_lock(key)` | 非阻塞，拿不到立刻返回 `false` |
| `pg_advisory_xact_lock(key)` | **事务级**，COMMIT / ROLLBACK 自动释放（推荐） |
| `pg_advisory_unlock(key)` | 手动释放会话级锁 |

<Callout type="info">
  优先用 **`_xact_` 事务级**版本。会话级锁(`pg_advisory_lock`)不随事务结束释放，一旦忘了 `unlock`，或连接异常挂了没清理，这把锁就一直悬着，别的进程永远拿不到——这是死锁 / 卡死的常见来源。事务级锁则由 PostgreSQL 在事务结束（哪怕是崩溃回滚）时**自动放掉**，省心得多。
</Callout>

第二个连接来抢同一个键，会拿不到——这正是它当锁用的意义：**跨进程、跨节点，只要连同一个数据库，就能靠约定的键互斥。**

**它和别的锁的区别：**

- **和 `SELECT ... FOR UPDATE` 比**：行锁必须锁住一条真实存在的行；咨询锁不需要——你可以为「一个还不存在的任务」「一个抽象的资源」加锁，纯凭键。
- **和 Redis `SETNX` 锁比**：免了 Redis 这个额外组件，而且**和 PG 事务联动**（事务回滚锁自动放），不像 Redis 锁要自己处理过期与续租。

## 行级锁：FOR UPDATE 家族

回到锁真实数据行的场景。`SELECT ... FOR UPDATE` 给选中的行加**排他锁**，直到事务结束——经典的「读出来、算一算、改回去」防并发覆盖。它有几个修饰：

- `FOR UPDATE` —— 拿不到就**等**，直到别人释放。
- `FOR UPDATE NOWAIT` —— 拿不到**立刻报错**，适合「拿不到就别干了」。
- `FOR UPDATE SKIP LOCKED` —— **跳过**已被别人锁住的行，继续往下挑没被锁的。

还有更弱的 `FOR NO KEY UPDATE` / `FOR SHARE` / `FOR KEY SHARE`，按「我打算改什么」锁不同粒度，减少不必要的阻塞。

## SKIP LOCKED：用数据库当任务队列

`SKIP LOCKED` 是这一章的重头戏，它是「**把 PostgreSQL 当任务队列**」这一流行模式的核心。想象 N 个 worker 同时来领任务：

```sql
-- 每个 worker 都跑这条:领一条「还没被别人占住」的待办任务
SELECT id, payload FROM jobs
WHERE status = 'pending'
ORDER BY created_at
FOR UPDATE SKIP LOCKED      -- ← 关键:跳过已被别的 worker 锁住的行
LIMIT 1;
-- 领到后 UPDATE status = 'processing',提交
```

<Callout type="info">
  没有 `SKIP LOCKED` 会怎样？所有 worker 都想锁「最早那条待办」，于是第 2 个之后全**排队等**第 1 个——退化成串行，一个都快不了。加上 `SKIP LOCKED`，被锁住的行被直接跳过，每个 worker 都能领到**不同的**下一条空闲任务，天然并行、零冲突。这就是为什么大量「基于数据库的任务队列」库（各语言都有）底层都是这一句——不用 Redis / RabbitMQ，一张表 + `SKIP LOCKED` 就是个可靠队列，还自带事务和持久化。
</Callout>

原理上，`FOR UPDATE` 会在行上做标记（把锁定者的事务信息写进元组），别的事务扫到被锁的行时，`SKIP LOCKED` 让它「看见但跳过」，而不是阻塞等待——于是查询返回的是「当前可领的行」的子集。

## 横向对比

| 能力 | PostgreSQL | MySQL | Oracle | SQL Server |
| --- | --- | --- | --- | --- |
| 咨询锁（键锁） | `pg_advisory_lock` 系列 | `GET_LOCK()`（具名锁，类似） | `DBMS_LOCK` | `sp_getapplock` |
| `FOR UPDATE` | 有 | 有 | 有 | `UPDLOCK` 提示 |
| `SKIP LOCKED` | 有 | **8.0+** 才有 | 很早就有 | `READPAST` 提示 |
| `NOWAIT` | 有 | 有 | 有 | 有 |

`SKIP LOCKED` 现在是 SQL 标准(SQL：2016)，Oracle 很早就有，**MySQL 直到 8.0(2018)才支持**——又是一个「老 MySQL 做不了、得靠外部队列」而 PostgreSQL 早就能做的例子。咨询锁各家都有类似物（MySQL 的 `GET_LOCK` 具名锁思路接近），但 PG 的整数键 + 事务联动用起来最顺手。

## 应用场景

- **任务队列**：一张 `jobs` 表 + `FOR UPDATE SKIP LOCKED`，多 worker 并行消费，免 Redis / MQ，还自带事务与持久化。
- **定时任务防重入**：多实例部署时，cron 任务开始前先 `pg_try_advisory_xact_lock(任务ID)`，拿不到就说明别的实例在跑，直接跳过——避免重复执行。
- **领导者选举**：多个节点抢同一个咨询锁，拿到的当 leader，事务 / 连接断了锁自动释放，别的节点顶上。
- **并发安全的「读改写」**：扣库存、改余额，`SELECT ... FOR UPDATE` 锁住目标行，杜绝并发覆盖。

## 小结与下一章

PostgreSQL 的两把锁覆盖了并发的两大需求：**咨询锁**让它当分布式锁（选主、防重入），**`FOR UPDATE SKIP LOCKED`** 让它当任务队列（多 worker 分活）。很多「一上来就要 Redis」的场景，其实一个 PG 就够了——还少维护一个系统、少一处一致性裂缝。

到这里，PostgreSQL 内核层面的特性基本讲完了。最后一站看它的**扩展生态**：`CREATE EXTENSION` 一句话，给数据库装上模糊搜索、加密、地理空间等一整套新能力。👉 [扩展生态：一句话给数据库装新能力](https://daiw.org/manual/postgresql/extensions)。

## 参考文献

- [1] PostgreSQL 官方文档.「13.3.5. Advisory Locks.」https://www.postgresql.org/docs/current/explicit-locking.html#ADVISORY-LOCKS —— 咨询锁语义与会话 / 事务级区别。
- [2] PostgreSQL 官方文档.「SELECT — The Locking Clause(FOR UPDATE / SKIP LOCKED).」https://www.postgresql.org/docs/current/sql-select.html#SQL-FOR-UPDATE-SHARE —— 行级锁修饰与 `SKIP LOCKED`。
- [3] PostgreSQL 官方文档.「9.28. Advisory Lock Functions.」https://www.postgresql.org/docs/current/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS —— 全部咨询锁函数。
