锁与并发 · 咨询锁与 SKIP LOCKED
咨询锁(advisory lock)是 PostgreSQL 提供的「应用层的锁」,纯靠整数键标识、不绑任何行,让 PG 直接当分布式锁用;而 SELECT ... FOR UPDATE SKIP LOCKED 跳过已被锁的行,是「用数据库当任务队列」的关键——N 个 worker 各领各的活,不打架、不用 Redis。
锁与并发 · 咨询锁与 SKIP LOCKED
并发场景里有两类经典需求:「同一件事,只允许一个进程在干」(防重入、选主),和「一堆任务,多个 worker 分着做、不重复领」(任务队列)。很多人第一反应是上 Redis。但 PostgreSQL 自带两把锁——咨询锁和行级锁 + SKIP LOCKED——把这两类需求都在数据库内解决了。
咨询锁:把 PostgreSQL 当分布式锁
咨询锁(Advisory Lock)是 PostgreSQL 里一种特别的锁:它不绑任何具体的行或表,纯粹靠一个(或两个)整数键来标识,含义完全由应用约定。数据库只负责「这个键当前有没有被别人占着」。
-- 事务级:非阻塞尝试拿键为 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) | 手动释放会话级锁 |
优先用 _xact_ 事务级版本。会话级锁(pg_advisory_lock)不随事务结束释放,一旦忘了 unlock,或连接异常挂了没清理,这把锁就一直悬着,别的进程永远拿不到——这是死锁 / 卡死的常见来源。事务级锁则由 PostgreSQL 在事务结束(哪怕是崩溃回滚)时自动放掉,省心得多。
第二个连接来抢同一个键,会拿不到——这正是它当锁用的意义:跨进程、跨节点,只要连同一个数据库,就能靠约定的键互斥。
它和别的锁的区别:
- 和
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 同时来领任务:
-- 每个 worker 都跑这条:领一条「还没被别人占住」的待办任务
SELECT id, payload FROM jobs
WHERE status = 'pending'
ORDER BY created_at
FOR UPDATE SKIP LOCKED -- ← 关键:跳过已被别的 worker 锁住的行
LIMIT 1;
-- 领到后 UPDATE status = 'processing',提交没有 SKIP LOCKED 会怎样?所有 worker 都想锁「最早那条待办」,于是第 2 个之后全排队等第 1 个——退化成串行,一个都快不了。加上 SKIP LOCKED,被锁住的行被直接跳过,每个 worker 都能领到不同的下一条空闲任务,天然并行、零冲突。这就是为什么大量「基于数据库的任务队列」库(各语言都有)底层都是这一句——不用 Redis / RabbitMQ,一张表 + SKIP LOCKED 就是个可靠队列,还自带事务和持久化。
原理上,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 一句话,给数据库装上模糊搜索、加密、地理空间等一整套新能力。👉 扩展生态:一句话给数据库装新能力。
参考文献
- [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 —— 全部咨询锁函数。