• Welcome to HiddenMerit - Clyde's Blog
  • Welcome to try the game Torn: Referral Link
  • If you are my relative, friend, or netizen, quickly press Ctrl+D to bookmark Clyde's Blog
  • This site has a like feature. If you read any article, please hit the like button so I know someone has visited
  • Email: hiddenmeritATgmail.com (replace AT with @)

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

DBA Clyde Jin 3周前 (04-24) 16次浏览 0个评论

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

锁机制与死锁处理实战

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

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

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

例如:

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

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


二、表级锁

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

锁模式 关键字 冲突对象 典型场景
ACCESS SHARE ACCESS SHARE 仅与 ACCESS EXCLUSIVE 冲突 SELECT 查询自动加此锁
ROW SHARE ROW SHARE EXCLUSIVEACCESS EXCLUSIVE 冲突 SELECT FOR UPDATE/SHARE
ROW EXCLUSIVE ROW EXCLUSIVE SHARESHARE ROW EXCLUSIVEEXCLUSIVEACCESS EXCLUSIVE 冲突 UPDATEDELETEINSERT
SHARE UPDATE EXCLUSIVE SHARE UPDATE EXCLUSIVE SHARE UPDATE EXCLUSIVESHARESHARE ROW EXCLUSIVEEXCLUSIVEACCESS EXCLUSIVE 冲突 VACUUMCREATE INDEX CONCURRENTLY
SHARE SHARE ROW EXCLUSIVESHARE UPDATE EXCLUSIVESHARE ROW EXCLUSIVEEXCLUSIVEACCESS EXCLUSIVE 冲突 保护表结构不被修改,但允许读
SHARE ROW EXCLUSIVE SHARE ROW EXCLUSIVE ROW EXCLUSIVESHARE UPDATE EXCLUSIVESHARESHARE ROW EXCLUSIVEEXCLUSIVEACCESS EXCLUSIVE 冲突 较少直接使用,类似保护整个表
EXCLUSIVE EXCLUSIVE ROW SHAREROW EXCLUSIVESHARE UPDATE EXCLUSIVESHARESHARE ROW EXCLUSIVEEXCLUSIVEACCESS EXCLUSIVE 冲突 允许读但阻止所有写和并发锁
ACCESS EXCLUSIVE ACCESS EXCLUSIVE 与所有锁模式冲突 DROP TABLETRUNCATEREINDEXVACUUM FULLALTER 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 查看,locktypetuple(旧版本)或 transactionid(行锁本质关联事务)。更直观的是通过 SELECT ... FOR UPDATE/SHARE 来加锁。

行锁模式 关键字 冲突(与另一个行锁) 说明
FOR KEY SHARE FOR KEY SHARE FOR UPDATEFOR NO KEY UPDATE 冲突,但与 FOR SHAREFOR KEY SHARE 兼容 只阻止键的更新(外键检查常用)
FOR SHARE FOR SHARE FOR UPDATEFOR NO KEY UPDATE 冲突,与 FOR SHAREFOR KEY SHARE 兼容 共享锁,防止被更新或删除
FOR NO KEY UPDATE FOR NO KEY UPDATE FOR UPDATE 冲突,与 FOR NO KEY UPDATEFOR SHAREFOR 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 秒),每隔这么长时间检查一次锁等待图。如果发现循环依赖,就选择一个事务作为牺牲品,回滚它并释放锁,同时返回错误:

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 COMMITTEDREPEATABLE READSERIALIZABLE 的行为边界
  • 什么是序列化异常?PostgreSQL 如何通过 SSI(可串行化快照隔离)检测?
  • 实战再现:幻读、写偏斜(Write Skew)的复现与规避
  • 应用层如何选择合适的隔离级别

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

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

绩隐金 , 版权所有丨如未注明 , 均为原创丨本网站采用BY-NC-SA协议进行授权
转载请注明原文链接:PostgreSQL 事务与并发系列 · 第三期
喜欢 (0)
发表我的评论
取消评论
表情 贴图 加粗 删除线 居中 斜体 签到

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址