核心内容摘要
广州卓远虚拟现实科技股份有限公司推出的百慕大动感影院享受前所未有的沉浸式航海体验
文章目录
死锁的本质循环等待
1 什么是死锁
2 示例典型双事务死锁
PostgreSQL 的死锁检测机制
1 检测触发时机
2 检测算法等待图Wait-for Graph
3 死锁解决策略
死锁日志详解如何读懂关键信息
1 日志字段解析
从日志定位死锁根源的五步法步骤 1提取死锁涉及的进程与表步骤 2还原事务完整操作序列步骤 3分析锁获取顺序步骤 4检查隐式锁来源步骤 5复现与验证
常见死锁模式与解决方案模式 1交叉更新Cross Update模式 2外键死锁模式 3唯一索引插入冲突模式 4批量操作无序
死锁预防与监控体系
1 配置优化
2 监控指标
3 应用层最佳实践
4 工具辅助
高级话题
1 死锁与隔离级别的关系
2 常用诊断 SQL在高并发的数据库系统中死锁Deadlock是一种典型的并发控制问题。
PostgreSQL 作为一款支持多版本并发控制MVCC和细粒度行级锁的数据库虽然在读写场景中具备良好的并发性能但在涉及显式更新、外键约束、唯一索引或复杂事务逻辑时仍可能发生死锁。
PostgreSQL 内置了高效的死锁检测机制能够在死锁发生后自动回滚其中一个事务以打破循环等待。
然而仅仅依赖自动回滚并不足以保障系统稳定性——开发人员和 DBA 必须能够从日志中快速定位死锁根源并采取措施预防其再次发生。
本文将从死锁的基本原理出发深入剖析 PostgreSQL 的死锁检测机制、日志格式解析、常见死锁模式并提供一套完整的排查与优化方法论。
死锁的本质循环等待PostgreSQL 的死锁检测机制高效可靠但“自动回滚”只是应急手段。
真正的工程能力体现在读懂死锁日志从进程、表、元组、SQL 还原现场识别根本模式交叉更新、外键、无序批量等实施预防策略统一顺序、缩短事务、重试机制建立监控体系及时发现死锁趋势防患于未然死锁不是 PostgreSQL 的缺陷而是并发系统的自然现象。
理解其原理掌握排查方法才能构建真正健壮的数据库应用。
提醒没有死锁的日志不等于没有死锁风险。
定期审查高并发事务逻辑比事后排查更为重要。
1 什么是死锁死锁是指两个或多个事务相互持有对方所需的资源如行锁、表锁且都在等待对方释放从而导致所有事务都无法继续执行的状态。
经典四条件Coffman 条件互斥条件资源一次只能被一个事务占用占有并等待事务持有资源的同时请求新资源不可抢占已分配的资源不能被强制收回循环等待存在一个事务等待环PostgreSQL 中最常见的死锁发生在行级锁Row-Level Locks上尤其是在UPDATE、DELETE或SELECT FOR UPDATE场景中。
2 示例典型双事务死锁-- 会话 ABEGIN;UPDATEaccountsSETbalancebalance-100WHEREid1;-- 持有 id1 的行锁-- 此时切换到会话 B-- 会话 BBEGIN;UPDATEaccountsSETbalancebalance50WHEREid2;-- 持有 id2 的行锁UPDATEaccountsSETbalancebalance-50WHEREid1;-- 等待 A 释放 id1-- 切回会话 AUPDATEaccountsSETbalancebalance100WHEREid2;-- 等待 B 释放 id2此时形成循环等待A → B → A构成死锁。
PostgreSQL 检测到后会回滚其中一个事务通常是后发起等待的并在日志中记录详细信息。
PostgreSQL 的死锁检测机制
1 检测触发时机PostgreSQL 并非实时检测死锁而是在以下情况下触发当一个事务尝试获取锁但被阻塞时阻塞时间超过deadlock_timeout默认 1 秒后台死锁检测器CheckDeadLock()被唤醒注意deadlock_timeout不是死锁发生的阈值而是开始检测的延迟。
设置过小会增加 CPU 开销过大则延长死锁发现时间。
2 检测算法等待图Wait-for GraphPostgreSQL 维护一个进程等待图节点为后端进程backend边表示“P1 等待 P2 持有的锁”。
当新等待关系加入时系统通过深度优先搜索DFS检查是否存在环。
若存在则判定为死锁。
检测范围包括行锁、表锁、轻量级锁LWLock、Advisory Lock 等支持跨事务、跨会话的死锁检测
3 死锁解决策略一旦检测到死锁PostgreSQL 会选择一个“代价最小”的事务进行回滚通常选择最后请求锁的事务若涉及多个锁类型优先回滚持有较少锁的事务回滚后释放其所有锁打破循环被回滚的事务会收到错误ERROR: deadlock detected DETAIL: Process 12345 waits for ShareLock on transaction 67890; blocked by process
54321.
死锁日志详解如何读懂关键信息要定位死锁根源必须学会解读 PostgreSQL 的死锁日志。
前提是开启相关日志配置log_min_messages warning log_min_error_statement error log_lock_waits on # 记录长时间锁等待 log_statement none # 可选避免日志过大当死锁发生时日志会输出类似以下内容PostgreSQL 14 格式
08:15:
2
456 UTC [12345] ERROR: deadlock detected
08:15:
2
456 UTC [12345] DETAIL: Process 12345 waits for ExclusiveLock on tuple (12,
of relation 16384 of database 16385 after
1
234 ms; blocked by process
Process 54321 waits for ExclusiveLock on tuple (45,
of relation 16384 of database 16385 after
9
123 ms; blocked by process
See server log for query details.
08:15:
2
456 UTC [12345] HINT: Run the ANALYZE command to update table and index statistics.
08:15:
2
456 UTC [12345] CONTEXT: while updating tuple (12,
in relation accounts
08:15:
2
456 UTC [12345] STATEMENT: UPDATE accounts SET balance balance 100 WHERE id 2;
1 日志字段解析字段含义Process XXXXX后端进程 ID对应pg_stat_activity.pidwaits for ExclusiveLock等待的锁类型常见ExclusiveLock,ShareLock,RowExclusiveLocktuple (12,
数据页号 12偏移量 3可通过ctid对应relation 16384表的 OID可通过SELECT oid, relname FROM pg_class WHERE oid 16384;查询database 16385数据库 OIDpg_databaseafter
1
234 ms等待时间接近deadlock_timeoutSTATEMENT触发死锁的 SQL 语句需log_min_error_statement error⚠️ 注意STATEMENT只显示当前语句不显示事务中之前执行的语句。
因此完整事务上下文需结合应用日志。
从日志定位死锁根源的五步法步骤 1提取死锁涉及的进程与表从日志中找出所有参与死锁的Process ID涉及的relation OID锁类型与元组位置示例进程 12345 和 54321表 OID 16384 →accounts互相等待对方持有的行锁步骤 2还原事务完整操作序列由于日志只显示“最后一句”需结合应用程序日志记录完整事务 SQLpg_stat_statements若启用代码逻辑事务边界、SQL 顺序关键问题两个事务是否以不同顺序访问相同行是否存在隐式锁如外键检查、唯一索引插入步骤 3分析锁获取顺序死锁的根本原因通常是锁获取顺序不一致。
例如事务 A先锁 id1再锁 id2事务 B先锁 id2再锁 id1解决方案强制所有事务按相同顺序访问资源如按主键升序。
步骤 4检查隐式锁来源并非所有锁都来自显式UPDATE。
以下操作也会加锁操作加锁类型说明INSERTROW EXCLUSIVE若有外键还会对父表加SHARE ROW EXCLUSIVEUPDATE唯一索引列SHAREon index可能引发索引页锁竞争ON DELETE CASCADE行锁 on 子表外键级联删除会锁子表行SELECT FOR UPDATEEXCLUSIVE显式行锁案例两个事务同时插入具有相同外键值的记录可能因父表行锁冲突导致死锁。
步骤 5复现与验证使用脚本模拟死锁场景# 伪代码模拟双事务交叉更新defsession_a():connconnect()curconn.cursor()cur.execute(BEGIN)cur.execute(UPDATE accounts SET balance balance - 100 WHERE id
time.sleep(
cur.execute(UPDATE accounts SET balance balance 100 WHERE id
# 死锁点conn.commit()defsession_b():connconnect()curconn.cursor()cur.execute(BEGIN)cur.execute(UPDATE accounts SET balance balance - 50 WHERE id
time.sleep(
cur.execute(UPDATE accounts SET balance balance 50 WHERE id
# 死锁点conn.commit()通过复现确认根因并验证修复方案。
常见死锁模式与解决方案模式 1交叉更新Cross Update现象两个事务以相反顺序更新多行。
解决方案应用层强制按主键/ID 升序更新使用ORDER BY在UPDATE前确定顺序但需注意UPDATE本身不保证顺序模式 2外键死锁现象事务 A 插入子表记录外键指向父表 id100事务 B 删除父表 id100A 需要父表共享锁B 需要排他锁 → 死锁解决方案避免在高并发下频繁删除父记录使用DEFERRABLE外键延迟检查到事务提交先删除子表再删除父表模式 3唯一索引插入冲突现象两个事务同时插入相同唯一键值各自持有部分索引页锁互相等待解决方案使用ON CONFLICT DO NOTHING/UPDATEUPSERT应用层先查后插但需注意 race condition模式 4批量操作无序现象批量UPDATE多行但输入列表顺序随机。
解决方案在应用层对 ID 列表排序后再执行分批次处理每批按固定顺序
死锁预防与监控体系
1 配置优化deadlock_timeout 1s # 默认值通常合适不建议过小 log_lock_waits on # 记录长时间锁等待早于死锁 log_min_duration_statement 1000 # 辅助分析慢查询
2 监控指标死锁次数pg_stat_database.deadlocksSELECTdatname,deadlocksFROMpg_stat_database;锁等待事件pg_stat_activity.wait_event_type Lock长事务可能加剧死锁概率
3 应用层最佳实践保持事务短小减少锁持有时间统一访问顺序按主键、时间戳等固定顺序操作数据避免交互式事务不要在事务中等待用户输入重试机制捕获deadlock detected错误后自动重试通常 1~3 次
4 工具辅助pgBadger分析日志统计死锁频率Prometheus postgres_exporter监控pg_stat_database.deadlocksauto_explain结合log_analyze on查看执行计划是否引发意外锁
高级话题
1 死锁与隔离级别的关系虽然 MVCC 减少了读写冲突但写-写冲突仍需加锁因此死锁与隔离级别关系如下READ COMMITTED最常见死锁场景每次语句获取新快照但写操作仍加锁REPEATABLE READ/SERIALIZABLE死锁可能性略低因快照一致减少部分冲突但仍会发生注意SSISerializable Snapshot Isolation用于检测写偏斜而非传统死锁。
死锁检测独立于 SSI。