PostgreSQL 事务与并发系列 · 第三期

PostgreSQL 事务与并发系列 · 第三期

锁机制与死锁处理实战

前两期我们深入了 MVCC 与快照隔离,知道了“读不阻塞写”。但有些场景必须靠锁来强制串行化。本期将带你掌握 PostgreSQL 中各类锁的用法、冲突关系、死锁成因与排查,并提供生产环境可用的监控脚本。

一、为什么有了 MVCC 还需要锁?

MVCC 主要解决了 读写冲突(读不阻塞写,写不阻塞读),但它无法解决 写写冲突 以及某些需要“绝对最新数据”的场景。

例如:

  • 两个事务同时对同一行执行 UPDATE,必须有一个先等另一个完成,否则更新会相互覆盖。
  • 业务上要求“先查询余额,余额足够才扣款”,如果不用锁就可能出现超卖。

因此,PostgreSQL 在 MVCC 之上仍然保留了完善的锁机制,包括表级锁、行级锁,以及用于应用层协调的咨询锁。


二、表级锁

表级锁由 PostgreSQL 内核自动管理,但你可以通过 LOCK 命令显式指定。表级锁按照冲突程度从低到高排列如下:

锁模式 关键字 冲突对象 典型场景
ACCESS SHARE ACCESS SHARE 仅与 ACCESS EXCLUSIVE 冲突 SELECT 查询自动加此锁
ROW SHARE ROW SHARE 与 EXCLUSIVE、ACCESS EXCLUSIVE 冲突 SELECT FOR UPDATE/SHARE
ROW EXCLUSIVE ROW EXCLUSIVE 与 SHARE、SHARE ROW EXCLUSIVE、EXCLUSIVE、ACCESS EXCLUSIVE 冲突 UPDATE、DELETE、INSERT
SHARE UPDATE EXCLUSIVE SHARE UPDATE EXCLUSIVE 与 SHARE UPDATE EXCLUSIVE、SHARE、SHARE ROW EXCLUSIVE、EXCLUSIVE、ACCESS EXCLUSIVE 冲突 VACUUM、CREATE INDEX CONCURRENTLY
SHARE SHARE 与 ROW EXCLUSIVE、SHARE UPDATE EXCLUSIVE、SHARE ROW EXCLUSIVE、EXCLUSIVE、ACCESS EXCLUSIVE 冲突 保护表结构不被修改,但允许读
SHARE ROW EXCLUSIVE SHARE ROW EXCLUSIVE 与 ROW EXCLUSIVE、SHARE UPDATE EXCLUSIVE、SHARE、SHARE ROW EXCLUSIVE、EXCLUSIVE、ACCESS EXCLUSIVE 冲突 较少直接使用,类似保护整个表
EXCLUSIVE EXCLUSIVE 与 ROW SHARE、ROW EXCLUSIVE、SHARE UPDATE EXCLUSIVE、SHARE、SHARE ROW EXCLUSIVE、EXCLUSIVE、ACCESS EXCLUSIVE 冲突 允许读但阻止所有写和并发锁
ACCESS EXCLUSIVE ACCESS EXCLUSIVE 与所有锁模式冲突 DROP TABLE、TRUNCATE、REINDEX、VACUUM FULL、ALTER TABLE 等

查看当前表级锁

SELECT relation::regclass AS table_name, 
       mode, 
       granted,
       pid
FROM pg_locks
WHERE locktype = 'relation'
  AND relation IS NOT NULL;

显式加锁示例

-- 防止其他事务对表进行 DDL 操作,但允许普通查询
LOCK TABLE my_table IN SHARE MODE;

-- 禁止任何并发写入,但允许读取
LOCK TABLE my_table IN EXCLUSIVE MODE;

-- 完全排他(用于维护)
LOCK TABLE my_table IN ACCESS EXCLUSIVE MODE;

三、行级锁

行级锁不会锁整个表,而是锁住特定的行版本。它们也是通过 pg_locks 查看,locktype 为 tuple(旧版本)或 transactionid(行锁本质关联事务)。更直观的是通过 SELECT ... FOR UPDATE/SHARE 来加锁。

行锁模式 关键字 冲突(与另一个行锁) 说明
FOR KEY SHARE FOR KEY SHARE 与 FOR UPDATE、FOR NO KEY UPDATE 冲突,但与 FOR SHARE、FOR KEY SHARE 兼容 只阻止键的更新(外键检查常用)
FOR SHARE FOR SHARE 与 FOR UPDATE、FOR NO KEY UPDATE 冲突,与 FOR SHARE、FOR KEY SHARE 兼容 共享锁,防止被更新或删除
FOR NO KEY UPDATE FOR NO KEY UPDATE 与 FOR UPDATE 冲突,与 FOR NO KEY UPDATE、FOR SHARE、FOR KEY SHARE 兼容 更新非键列时自动加此锁
FOR UPDATE FOR UPDATE 与所有其他行锁冲突 排他锁,禁止其他任何并发修改或加锁

典型用法

-- 锁定选中的行,防止其他事务更新/删除,直到事务结束
BEGIN;
SELECT * FROM accounts WHERE id = 1 FOR UPDATE;
-- 此时可以安全地基于当前余额做逻辑
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT;

-- 共享模式:只阻止更新或删除,但不阻止其他事务加共享锁
SELECT * FROM accounts WHERE id = 1 FOR SHARE;

SKIP LOCKED 与 NOWAIT

  • NOWAIT:无法立即获得锁时立即报错,而不等待。
  • SKIP LOCKED:跳过已经被其他事务锁定的行,只返回未锁定的行。非常适合任务队列(多个 worker 抢任务)。
-- 从任务表中取一条未锁定的任务
SELECT * FROM jobs WHERE status = 'pending'
ORDER BY created_at LIMIT 1
FOR UPDATE SKIP LOCKED;

四、咨询锁(Advisory Locks)

咨询锁是一种应用层逻辑锁,完全由应用程序负责获取和释放,不绑定具体的表或行。可用于跨进程协调、防止并发执行同一段业务逻辑。

-- 获取一个会话级别的咨询锁(整型键)
SELECT pg_advisory_lock(12345);

-- 尝试获取,如果拿不到就跳过
SELECT pg_try_advisory_lock(12345);

-- 释放
SELECT pg_advisory_unlock(12345);

-- 事务级别的咨询锁,随事务自动释放
SELECT pg_advisory_xact_lock(12345);

典型场景:定时任务防止重叠执行、消息队列去重、分布式计数等。


五、死锁:成因与检测

5.1 死锁示例

时间 事务 A 事务 B
T1 BEGIN; BEGIN;
T2 UPDATE accounts SET balance=balance-100 WHERE id=1; (获得 id=1 的行锁)
T3 UPDATE accounts SET balance=balance+100 WHERE id=2; (获得 id=2 的行锁)
T4 UPDATE accounts SET balance=balance+100 WHERE id=2; (等待事务 B 释放 id=2 的锁)
T5 UPDATE accounts SET balance=balance-100 WHERE id=1; (等待事务 A 释放 id=1 的锁)

此时互相等待 ⇒ 死锁。

5.2 PostgreSQL 的死锁检测

PostgreSQL 有一个后台进程 deadlock_timeout(默认 1 秒),每隔这么长时间检查一次锁等待图。如果发现循环依赖,就选择一个事务作为牺牲品,回滚它并释放锁,同时返回错误:

[quads id="805"]

ERROR: deadlock detected

5.3 如何定位死锁原因

  • 查看 PostgreSQL 日志(设置 log_lock_waits = on,死锁会被详细记录)。
  • 查询当前锁等待情况:
-- 查看当前阻塞与被阻塞的会话
SELECT blocked_locks.pid     AS blocked_pid,
       blocked_activity.query AS blocked_query,
       blocking_locks.pid     AS blocking_pid,
       blocking_activity.query AS blocking_query
FROM pg_catalog.pg_locks blocked_locks
JOIN pg_catalog.pg_stat_activity blocked_activity ON blocked_activity.pid = blocked_locks.pid
JOIN pg_catalog.pg_locks blocking_locks ON blocking_locks.locktype = blocked_locks.locktype
  AND blocking_locks.relation = blocked_locks.relation
  AND blocking_locks.page = blocked_locks.page
  AND blocking_locks.tuple = blocked_locks.tuple
  AND blocking_locks.virtualxid = blocked_locks.virtualxid
  AND blocking_locks.transactionid = blocked_locks.transactionid
  AND blocking_locks.classid = blocked_locks.classid
  AND blocking_locks.objid = blocked_locks.objid
  AND blocking_locks.objsubid = blocked_locks.objsubid
  AND blocking_locks.pid != blocked_locks.pid
JOIN pg_catalog.pg_stat_activity blocking_activity ON blocking_activity.pid = blocking_locks.pid
WHERE NOT blocked_locks.granted;

更简洁的视图(可考虑用 pg_blocking_pids(pid) 函数):

SELECT pid, 
       usename, 
       state, 
       query,
       pg_blocking_pids(pid) AS blocked_by
FROM pg_stat_activity
WHERE cardinality(pg_blocking_pids(pid)) > 0;

5.4 避免死锁的最佳实践

  1. 统一访问顺序:如果多个事务需要更新多张表或多行,按相同的顺序加锁(例如总是先锁 id=1 再锁 id=2)。
  2. 尽可能缩短事务:不要在事务中做耗时的业务逻辑或等待用户交互。
  3. 使用 NOWAIT:如果不能立即获得锁,就让事务快速失败并重试,而不是等待造成死锁风险。
  4. 适当使用 SKIP LOCKED:对于任务队列类应用,完全避免锁冲突。
  5. 索引设计:确保 WHERE 条件能准确命中索引,避免表级锁升级(例如缺失索引会导致行锁升级为表锁的误解?实际上 PostgreSQL 行锁不会自动升级,但是缺少索引会导致锁定更多行,增加死锁概率)。

六、综合排查脚本示例

6.1 找出持有行锁却长时间不提交的事务

SELECT pid, 
       now() - xact_start AS duration,
       state,
       query
FROM pg_stat_activity
WHERE pid IN (
    SELECT pid FROM pg_locks WHERE locktype = 'transactionid')
  AND state != 'idle'
ORDER BY duration DESC;

6.2 强制终止阻塞的事务

-- 先查看 pid
SELECT pid, query, state FROM pg_stat_activity WHERE state = 'active' AND ...;
-- 终止(会回滚事务)
SELECT pg_terminate_backend(pid);

6.3 监控死锁发生频率(从日志解析)

如果你启用了 log_lock_waits = on 并设置了 deadlock_timeout,可以从 PostgreSQL 日志(默认在 pg_log 目录)中搜索 deadlock detected。


七、总结与下期预告

本期我们完整解读了 PostgreSQL 的锁体系:

  • 表级锁冲突矩阵与自动加锁行为
  • 行级锁(FOR UPDATE/SHARE)及其与 MVCC 的配合
  • 咨询锁的应用场景
  • 死锁的成因、检测机制和避免策略
  • 实用的锁监控和终止脚本

掌握了这些,你已经可以应对绝大多数并发冲突场景。

第四期预告:事务隔离级别深度测试与序列化异常

  • 深入分析 READ COMMITTED、REPEATABLE READ、SERIALIZABLE 的行为边界
  • 什么是序列化异常?PostgreSQL 如何通过 SSI(可串行化快照隔离)检测?
  • 实战再现:幻读、写偏斜(Write Skew)的复现与规避
  • 应用层如何选择合适的隔离级别

第四期将带你从理论走向实战调优,敬请期待!

有任何锁相关的疑难杂症(比如锁等待迟迟不释放、死锁频繁出现),欢迎留言区交流,我们下期见!

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top