纲要
死锁(Deadlock)的定义与形成条件- PostgreSQL 的死锁检测机制:
deadlock_timeout参数 - 死锁的预防、检测与恢复策略
- 单机环境下的死锁复现:行级锁与事务交错
- 死锁检测的牺牲者选择逻辑:常规事务与
autovacuum的特殊处理 - 流复制(Streaming Replication)场景下的死锁:备库
startup进程与用户查询的冲突 UPDATE语句的锁获取顺序与死锁:顺序扫描 vs 索引扫描- 加锁原则:够用即可、越快越好、递增获取、相同顺序
死锁的定义与形成条件
死锁(Deadlock)是并发系统中一个经典的问题。在 PostgreSQL 的语境下,死锁指的是两个或多个事务(Transaction)各自持有对方所需要的资源(如行锁、表锁),并且在等待对方释放资源的过程中形成循环依赖,导致所有事务都无法继续执行。
从锁的视角来看,死锁的形成需要满足四个必要条件:互斥条件、持有并等待条件、不可抢占条件以及循环等待条件。在 PostgreSQL 中,当两个事务以交错的方式获取同一组资源上的锁时,循环等待便可能发生。
死锁的预防、检测与恢复
PostgreSQL 对死锁的处理采用了一套“预防—检测—恢复”的完整机制。
死锁预防是一种事前手段,通过设计上的约束来保证系统不会进入死锁状态。例如,强制所有事务以相同的顺序获取锁。但预防措施往往伴随着性能开销(如排队等待),在锁冲突不严重的场景下可能造成不必要的资源浪费。
死锁检测是 PostgreSQL 的核心机制。由于死锁检测的开销相对较大,服务器并不会在每次等待锁时都执行检测。相反,PostgreSQL 会乐观地假设生产环境中死锁并不常见,在等待锁一段时间后才进行检查。这个等待时间由 deadlock_timeout 参数控制:
-- 查看当前 deadlock_timeout 设置(默认值为 1s)
SHOW deadlock_timeout;
-- 设置 deadlock_timeout(单位:毫秒)
SET deadlock_timeout = '2s';
deadlock_timeout 的默认值为 1 秒(1s),这通常是实践中最小的推荐值。在负载较高的服务器上,可以考虑适当增大该值。理想情况下,该设置应超过典型的事务执行时间,以提高锁在被等待者决定检查死锁之前得到释放的概率。当 log_lock_waits 参数启用时,deadlock_timeout 还决定了记录锁等待日志消息的阈值时间。
死锁恢复是检测到死锁后的处理动作。PostgreSQL 会选择一个事务作为牺牲者(Victim)予以终止(Abort),并回滚其所有操作,从而打破循环等待,让其他事务得以继续执行。
单机环境下的死锁复现
以下示例演示了在一个单机 PostgreSQL 实例中,两个事务因交错更新同一张表的不同行而引发死锁。
准备测试数据
DROP TABLE IF EXISTS t_lock_test;
CREATE TABLE t_lock_test (id INT PRIMARY KEY);
INSERT INTO t_lock_test VALUES (1), (2);
事务一(Session 1)
BEGIN;
UPDATE t_lock_test SET id = 99 WHERE id = 1;
-- 持有 id=1 的行锁,暂不提交
事务二(Session 2)
BEGIN;
UPDATE t_lock_test SET id = 100 WHERE id = 2;
-- 持有 id=2 的行锁,暂不提交
此时,事务一持有行 id=1 的锁,事务二持有行 id=2 的锁。接下来,两个事务尝试获取对方已持有的锁:
事务一(Session 1)继续执行
UPDATE t_lock_test SET id = 101 WHERE id = 2;
-- 等待事务二释放 id=2 的行锁
事务二(Session 2)继续执行
UPDATE t_lock_test SET id = 102 WHERE id = 1;
-- 等待事务一释放 id=1 的行锁
此时,事务一等待事务二,事务二等待事务一,形成了循环等待。PostgreSQL 的死锁检测器在 deadlock_timeout 超时后被唤醒,检测到死锁并终止其中一个事务。服务器日志中会出现类似以下的提示:
ERROR: deadlock detected
DETAIL: Process 12345 waits for ShareLock on transaction 67890; blocked by process 12346.
Process 12346 waits for ShareLock on transaction 67891; blocked by process 12345.
HINT: See server log for query details.
STATEMENT: UPDATE t_lock_test SET id = 102 WHERE id = 1;
死锁检测的牺牲者选择
大多数情况下,发起死锁检测的事务(即触发检测的那个事务)会被选为牺牲者并予以中断。但存在一个重要的特例:如果死锁循环链路中包含 autovacuum 进程,并且该进程正在执行防止事务 ID 回卷(Transaction ID Wraparound)的冻结操作(即 VACUUM FREEZE),PostgreSQL 会优先终止其他事务,以保全 autovacuum 进程。这是为了避免因事务 ID 回卷导致数据库只读或宕机的严重后果。
流复制场景下的死锁
死锁不仅会发生在单机环境中,在流复制(Streaming Replication)架构下同样可能出现。
在流复制中,备库(Standby)通过 startup 进程持续回放(Replay)从主库接收到的 WAL 日志。当主库执行需要获取 ACCESS EXCLUSIVE 锁的 DDL 操作(如 ALTER TABLE ADD COLUMN)时,备库的 startup 进程也会尝试获取同样的锁以回放该操作。
考虑以下场景:
- 主库对表
items执行ALTER TABLE ADD COLUMN,获取ACCESS EXCLUSIVE锁。 - 备库上,用户执行
SELECT * FROM items,获取ACCESS SHARE锁。 - 备库的
startup进程尝试回放主库的ALTER TABLE操作,需要获取ACCESS EXCLUSIVE锁,但被用户的SELECT查询阻塞。 - 此时,如果用户在备库上对另一张表执行查询,且该查询与
startup进程正在等待的锁形成交叉依赖,死锁便可能发生。
这种场景下的死锁,本质上是备库的回放进程(startup)与用户查询进程之间,因锁的获取顺序不一致而产生的循环等待。
UPDATE 语句的锁获取顺序与死锁
UPDATE 语句在更新多行时,其行锁的获取是逐行进行的,而非一次性锁定所有目标行。这意味着,如果两个 UPDATE 语句以不同的顺序扫描并锁定同一组行,就可能产生死锁。
-- 创建测试表并插入数据
CREATE TABLE t_update_test (id INT PRIMARY KEY, val INT);
INSERT INTO t_update_test SELECT generate_series(1, 10000), 0;
-- 创建倒序索引(用于模拟不同的扫描顺序)
CREATE INDEX idx_t_update_test_id_desc ON t_update_test (id DESC);
事务一:顺序扫描(从前往后)
BEGIN;
UPDATE t_update_test SET val = 1 WHERE id BETWEEN 1 AND 10000;
-- 从 id=1 开始依次获取行锁
事务二:利用倒序索引扫描(从后往前)
BEGIN;
SET enable_seqscan = OFF; -- 强制走索引扫描
UPDATE t_update_test SET val = 2 WHERE id BETWEEN 1 AND 10000;
-- 从 id=10000 开始依次获取行锁
两个事务以相反的顺序获取行锁,当它们交错执行时,便可能形成死锁。这个例子说明,即使是一个单独的 UPDATE 语句,如果其执行计划(顺序扫描 vs 索引扫描)导致锁获取顺序不同,也可能在并发环境下引发死锁。因此,不能简单地认为单个 SQL 语句是“原子”的,而需要关注其锁获取的具体行为。
加锁原则
基于对锁机制和死锁的理解,可以总结出以下加锁原则,以指导应用程序的设计:
够用即可
使用满足条件的最弱锁模式。例如:
- 仅需读取数据时,使用普通的
SELECT(获取ACCESS SHARE锁),而非SELECT FOR UPDATE或SELECT FOR SHARE。 - 创建索引时,优先考虑
CREATE INDEX CONCURRENTLY(获取SHARE UPDATE EXCLUSIVE锁),而非普通的CREATE INDEX(获取SHARE锁)。
-- 推荐:并发创建索引,允许并发读写
CREATE INDEX CONCURRENTLY idx_name ON table_name (column);
-- 不推荐:普通创建索引,会阻塞写操作
CREATE INDEX idx_name ON table_name (column);
越快越好
用“长时间的弱锁 + 短时间的强锁”替代“长时间的强锁”。例如,pg_repack 工具的核心原理就是通过短暂持有 ACCESS EXCLUSIVE 锁来完成表的重建,其余时间仅持有弱锁,从而降低对业务的冲击。
递增获取
避免使用激进的锁模式。仅在必要时才升级锁级别,不需要时尽早释放。
相同顺序
在多个事务中,以一致的顺序获取锁资源。从前面的死锁复现示例可以看出,事务交错运行(即锁获取顺序不一致)是死锁的根本原因。无论是单机环境下的行级锁,还是流复制环境下的表级锁,遵循相同的锁获取顺序都是预防死锁最有效的手段。
API 速览
deadlock_timeout
- 所属:PostgreSQL 服务器配置参数(GUC)
- 类型:
integer(毫秒) - 默认值:
1000(1秒) - 作用域:全局 / 会话 / 事务
- 权限:仅超级用户和具有适当
SET权限的用户可更改 - 说明:等待锁之后、检查死锁条件之前的等待时间。死锁检查开销较大,因此服务器不会在每次等待锁时都执行。
-- 查看当前值
SHOW deadlock_timeout;
-- 会话级设置
SET deadlock_timeout = '2s';
-- 全局设置(需重启或 reload)
ALTER SYSTEM SET deadlock_timeout = '2s';
SELECT pg_reload_conf();
log_lock_waits
- 所属:PostgreSQL 服务器配置参数(GUC)
- 类型:
boolean - 默认值:
off - 说明:当设置为
on时,任何等待锁超过deadlock_timeout时间的操作都会被记录到日志中。
-- 启用锁等待日志
SET log_lock_waits = on;
pg_locks 系统视图
- 所属:PostgreSQL 系统目录
- 说明:提供当前数据库服务器中所有未完成锁的列表。可用于监控锁等待和死锁诊断。
-- 查看当前所有未授予的锁(即等待中的锁)
SELECT * FROM pg_locks WHERE NOT granted;
-- 查看锁等待关系
SELECT blocked.pid AS blocked_pid,
blocking.pid AS blocking_pid,
blocked.query AS blocked_query
FROM pg_locks blocked
JOIN pg_locks blocking ON blocked.locktype = blocking.locktype
AND blocked.relation = blocking.relation
AND blocked.page = blocking.page
AND blocked.tuple = blocking.tuple
AND blocked.virtualxid = blocking.virtualxid
AND blocked.transactionid = blocking.transactionid
AND blocked.classid = blocking.classid
AND blocked.objid = blocking.objid
AND blocked.objsubid = blocking.objsubid
WHERE NOT blocked.granted
AND blocking.granted;
LOCK 命令
- 所属:PostgreSQL SQL 命令
- 语法:
LOCK [TABLE] name [, ...] [IN lockmode MODE] [NOWAIT] - 说明:显式获取表级锁。支持
ACCESS SHARE、ROW SHARE、ROW EXCLUSIVE、SHARE UPDATE EXCLUSIVE、SHARE、SHARE ROW EXCLUSIVE、EXCLUSIVE、ACCESS EXCLUSIVE等模式。
-- 以 ACCESS EXCLUSIVE 模式锁定表,若无法立即获取则报错而非等待
LOCK TABLE my_table IN ACCESS EXCLUSIVE MODE NOWAIT;
Demo 简单示例
以下是一个使用 Node.js 和 pg 驱动演示死锁检测与处理的完整示例。
运行说明
- 确保本地 PostgreSQL 实例运行中,且已创建测试数据库。
- 安装依赖:
npm install pg - 执行脚本:
node deadlock_demo.js
代码说明
该示例模拟了两个并发事务以相反的顺序更新同一张表的两行数据,从而触发死锁。代码展示了:
- 使用
pg连接池管理数据库连接 - 并发执行两个事务
- 捕获并处理
deadlock detected错误 - 事务失败后的回滚与重试逻辑
技术点总结
- PostgreSQL 行级锁的获取与释放机制
deadlock_timeout对死锁检测时机的影响- 死锁错误的捕获与业务层重试策略
- 事务隔离级别与锁的交互
import { Pool } from 'pg';
const pool = new Pool({
host: 'localhost',
port: 5432,
database: 'testdb',
user: 'postgres',
password: 'postgres',
max: 10,
});
async function setup() {
const client = await pool.connect();
await client.query(`
DROP TABLE IF EXISTS t_demo;
CREATE TABLE t_demo (id INT PRIMARY KEY, val INT);
INSERT INTO t_demo VALUES (1, 0), (2, 0);
`);
client.release();
}
async function runTransaction(
client: any,
updateId: number,
waitId: number,
txName: string
): Promise<void> {
try {
await client.query('BEGIN');
// 更新当前行,持有行锁
await client.query('UPDATE t_demo SET val = val + 1 WHERE id = $1', [updateId]);
console.log(`[${txName}] 已更新 id=${updateId},持有行锁`);
// 模拟业务处理延迟,增加死锁概率
await new Promise((resolve) => setTimeout(resolve, 500));
// 尝试更新另一行,可能被阻塞
console.log(`[${txName}] 尝试更新 id=${waitId},等待行锁...`);
await client.query('UPDATE t_demo SET val = val + 1 WHERE id = $1', [waitId]);
console.log(`[${txName}] 成功更新 id=${waitId}`);
await client.query('COMMIT');
console.log(`[${txName}] 事务提交成功`);
} catch (err: any) {
await client.query('ROLLBACK');
if (err.code === '40P01') {
// 40P01 是 PostgreSQL 的死锁错误代码
console.log(`[${txName}] 检测到死锁,事务已回滚: ${err.message}`);
} else {
console.log(`[${txName}] 事务失败: ${err.message}`);
}
throw err;
}
}
async function main() {
await setup();
const client1 = await pool.connect();
const client2 = await pool.connect();
// 事务一:先更新 id=1,再尝试更新 id=2
// 事务二:先更新 id=2,再尝试更新 id=1
// 两个事务以相反的顺序获取锁,必然产生死锁
const p1 = runTransaction(client1, 1, 2, 'Tx1');
const p2 = runTransaction(client2, 2, 1, 'Tx2');
await Promise.allSettled([p1, p2]);
client1.release();
client2.release();
await pool.end();
}
main().catch(console.error);
对应的 PostgreSQL 原生 SQL 操作(供参考):
-- Session 1
BEGIN;
UPDATE t_demo SET val = val + 1 WHERE id = 1;
-- 等待 500ms
UPDATE t_demo SET val = val + 1 WHERE id = 2;
COMMIT;
-- Session 2
BEGIN;
UPDATE t_demo SET val = val + 1 WHERE id = 2;
-- 等待 500ms
UPDATE t_demo SET val = val + 1 WHERE id = 1;
COMMIT;
多语言示例
Go 示例
使用 lib/pq 驱动(或 pgx)演示死锁检测与重试。
package main
import (
"database/sql"
"fmt"
"log"
"sync"
"time"
_ "github.com/lib/pq"
)
const connStr = "postgres://postgres:postgres@localhost/testdb?sslmode=disable"
func setup(db *sql.DB) error {
_, err := db.Exec(`
DROP TABLE IF EXISTS t_demo;
CREATE TABLE t_demo (id INT PRIMARY KEY, val INT);
INSERT INTO t_demo VALUES (1, 0), (2, 0);
`)
return err
}
func runTransaction(db *sql.DB, updateId, waitId int, txName string, wg *sync.WaitGroup) {
defer wg.Done()
tx, err := db.Begin()
if err != nil {
log.Printf("[%s] 开启事务失败: %v", txName, err)
return
}
defer tx.Rollback()
// 更新当前行,持有行锁
_, err = tx.Exec("UPDATE t_demo SET val = val + 1 WHERE id = $1", updateId)
if err != nil {
log.Printf("[%s] 更新 id=%d 失败: %v", txName, updateId, err)
return
}
log.Printf("[%s] 已更新 id=%d,持有行锁", txName, updateId)
// 模拟业务延迟
time.Sleep(500 * time.Millisecond)
// 尝试更新另一行
log.Printf("[%s] 尝试更新 id=%d,等待行锁...", txName, waitId)
_, err = tx.Exec("UPDATE t_demo SET val = val + 1 WHERE id = $1", waitId)
if err != nil {
// 检查是否为死锁错误(PostgreSQL 错误码 40P01)
if pgErr, ok := err.(*pq.Error); ok && pgErr.Code == "40P01" {
log.Printf("[%s] 检测到死锁,事务已回滚: %v", txName, err)
} else {
log.Printf("[%s] 更新失败: %v", txName, err)
}
return
}
log.Printf("[%s] 成功更新 id=%d", txName, waitId)
if err = tx.Commit(); err != nil {
log.Printf("[%s] 提交事务失败: %v", txName, err)
return
}
log.Printf("[%s] 事务提交成功", txName)
}
func main() {
db, err := sql.Open("postgres", connStr)
if err != nil {
log.Fatal(err)
}
defer db.Close()
if err := setup(db); err != nil {
log.Fatal(err)
}
var wg sync.WaitGroup
wg.Add(2)
// 事务一:先更新 id=1,再尝试 id=2
go runTransaction(db, 1, 2, "Tx1", &wg)
// 事务二:先更新 id=2,再尝试 id=1
go runTransaction(db, 2, 1, "Tx2", &wg)
wg.Wait()
log.Println("所有事务执行完毕")
}
运行说明:
- 安装依赖:
go get github.com/lib/pq - 运行:
go run deadlock_demo.go
Python 示例
使用 psycopg2 驱动。
import psycopg2
import psycopg2.extensions
import time
import threading
conn_str = "dbname=testdb user=postgres password=postgres host=localhost"
def setup():
conn = psycopg2.connect(conn_str)
conn.autocommit = True
cur = conn.cursor()
cur.execute("""
DROP TABLE IF EXISTS t_demo;
CREATE TABLE t_demo (id INT PRIMARY KEY, val INT);
INSERT INTO t_demo VALUES (1, 0), (2, 0);
""")
cur.close()
conn.close()
def run_transaction(update_id, wait_id, tx_name):
conn = psycopg2.connect(conn_str)
conn.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT) # 实际手动管理事务
conn.autocommit = False
cur = conn.cursor()
try:
cur.execute("BEGIN;")
cur.execute("UPDATE t_demo SET val = val + 1 WHERE id = %s;", (update_id,))
print(f"[{tx_name}] 已更新 id={update_id},持有行锁")
time.sleep(0.5)
print(f"[{tx_name}] 尝试更新 id={wait_id},等待行锁...")
cur.execute("UPDATE t_demo SET val = val + 1 WHERE id = %s;", (wait_id,))
print(f"[{tx_name}] 成功更新 id={wait_id}")
conn.commit()
print(f"[{tx_name}] 事务提交成功")
except psycopg2.errors.DeadlockDetected as e:
conn.rollback()
print(f"[{tx_name}] 检测到死锁,事务已回滚: {e}")
except Exception as e:
conn.rollback()
print(f"[{tx_name}] 事务失败: {e}")
finally:
cur.close()
conn.close()
if __name__ == "__main__":
setup()
t1 = threading.Thread(target=run_transaction, args=(1, 2, "Tx1"))
t2 = threading.Thread(target=run_transaction, args=(2, 1, "Tx2"))
t1.start()
t2.start()
t1.join()
t2.join()
print("所有事务执行完毕")
运行说明:
- 安装依赖:
pip install psycopg2-binary - 运行:
python deadlock_demo.py
Java 示例
使用 JDBC 和 PostgreSQL 驱动。
import java.sql.*;
import java.util.concurrent.CountDownLatch;
public class DeadlockDemo {
private static final String URL = "jdbc:postgresql://localhost/testdb";
private static final String USER = "postgres";
private static final String PASSWORD = "postgres";
public static void setup() throws SQLException {
try (Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
Statement stmt = conn.createStatement()) {
stmt.execute("DROP TABLE IF EXISTS t_demo");
stmt.execute("CREATE TABLE t_demo (id INT PRIMARY KEY, val INT)");
stmt.execute("INSERT INTO t_demo VALUES (1, 0), (2, 0)");
}
}
public static void runTransaction(int updateId, int waitId, String txName, CountDownLatch latch) {
try (Connection conn = DriverManager.getConnection(URL, USER, PASSWORD)) {
conn.setAutoCommit(false);
try (PreparedStatement pstmt1 = conn.prepareStatement("UPDATE t_demo SET val = val + 1 WHERE id = ?")) {
pstmt1.setInt(1, updateId);
pstmt1.executeUpdate();
System.out.println("[" + txName + "] 已更新 id=" + updateId + ",持有行锁");
Thread.sleep(500);
System.out.println("[" + txName + "] 尝试更新 id=" + waitId + ",等待行锁...");
try (PreparedStatement pstmt2 = conn.prepareStatement("UPDATE t_demo SET val = val + 1 WHERE id = ?")) {
pstmt2.setInt(1, waitId);
pstmt2.executeUpdate();
System.out.println("[" + txName + "] 成功更新 id=" + waitId);
}
conn.commit();
System.out.println("[" + txName + "] 事务提交成功");
} catch (SQLException e) {
conn.rollback();
// PostgreSQL 死锁错误码为 40P01
if ("40P01".equals(e.getSQLState())) {
System.out.println("[" + txName + "] 检测到死锁,事务已回滚: " + e.getMessage());
} else {
System.out.println("[" + txName + "] 事务失败: " + e.getMessage());
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
conn.rollback();
System.out.println("[" + txName + "] 事务被中断");
}
} catch (SQLException e) {
System.out.println("[" + txName + "] 数据库连接失败: " + e.getMessage());
} finally {
latch.countDown();
}
}
public static void main(String[] args) throws Exception {
setup();
CountDownLatch latch = new CountDownLatch(2);
Thread t1 = new Thread(() -> runTransaction(1, 2, "Tx1", latch));
Thread t2 = new Thread(() -> runTransaction(2, 1, "Tx2", latch));
t1.start();
t2.start();
latch.await();
System.out.println("所有事务执行完毕");
}
}
运行说明:
- 添加 PostgreSQL JDBC 驱动依赖(如 Maven:
org.postgresql:postgresql:42.6.0) - 编译:
javac DeadlockDemo.java - 运行:
java DeadlockDemo
多语言对比表格
| 特性 | Node.js (pg) | Go (lib/pq) | Python (psycopg2) | Java (JDBC) |
|---|---|---|---|---|
| 驱动/库 | pg | github.com/lib/pq | psycopg2 | PostgreSQL JDBC Driver |
| 连接池 | 内置 Pool | 需自行实现或使用 pgx 连接池 | 使用 psycopg2.pool 或第三方 | 可使用 HikariCP 等 |
| 事务管理 | BEGIN/COMMIT/ROLLBACK | Begin/Commit/Rollback | conn.autocommit=False 手动提交 | conn.setAutoCommit(false) |
| 错误码判断 | err.code === '40P01' | 类型断言为 *pq.Error,检查 Code | 捕获 psycopg2.errors.DeadlockDetected | 检查 SQLException.getSQLState() 是否为 "40P01" |
| 异步/并发 | async/await + Promise.all | Goroutine + sync.WaitGroup | threading.Thread | Thread + CountDownLatch |
| 延迟模拟 | setTimeout + Promise | time.Sleep | time.sleep | Thread.sleep |
| 错误处理 | try/catch | 错误返回或 log | try/except | try/catch |
| 依赖安装 | npm install pg | go get github.com/lib/pq | pip install psycopg2-binary | 通过 Maven/Gradle 添加 |
| 典型适用场景 | Node.js 后端服务 | 高性能微服务、云原生 | 数据科学、自动化脚本 | 企业级应用、Spring 生态 |
所有示例均演示了相同的死锁触发逻辑(两个事务以相反顺序更新两行数据),并捕获了 PostgreSQL 的死锁错误码(40P01)进行相应处理。实际生产中,建议结合重试机制(如指数退避)和更完善的日志监控。
项目难点与解决方案
核心难点
在生产环境中,死锁的发生往往是偶发且难以复现的。由于并发事务的交错执行受到多种因素(如网络延迟、应用负载、查询执行计划变化等)的影响,死锁的复现和诊断具有较高的复杂度。
解决方案
- 启用
log_lock_waits:将log_lock_waits设置为on,并配合合理的deadlock_timeout值,可以在日志中捕获锁等待和死锁的详细信息,便于事后分析。 - 监控
pg_locks与pg_stat_activity:定期查询锁等待关系,及时发现潜在的死锁风险。 - 应用层重试机制:在业务代码中捕获
40P01(死锁)错误码,实施有限次数的重试。 - 规范锁获取顺序:在应用设计层面,确保所有事务以相同的顺序访问数据库资源。
广度
死锁问题涉及 PostgreSQL 的锁管理器、事务管理器、WAL 日志、流复制等多个子系统,需要从单机到分布式架构的全局视角进行考量。
深度
死锁的诊断需要深入理解 PostgreSQL 的锁模式兼容性矩阵、行级锁与表级锁的交互、deadlock_timeout 与 log_lock_waits 的协同工作机制,以及 autovacuum 等后台进程的特殊处理逻辑。
复杂度
死锁问题的复杂度随着并发事务数量、涉及的表数量、锁模式多样性以及流复制架构的引入而呈指数级增长。在生产环境中,死锁的根因分析往往需要结合应用日志、数据库日志、系统视图快照等多维度数据进行综合判断。
官方文档
- PostgreSQL 官方文档:锁管理 (Lock Management)
- PostgreSQL 官方文档:显式锁定 (Explicit Locking)
- PostgreSQL 官方文档:pg_locks 系统视图
- PostgreSQL 官方文档:LOCK 命令
参考链接
总结
本文围绕 PostgreSQL 的死锁问题,从定义、检测机制、恢复策略到具体的复现案例进行了系统性的梳理。死锁的本质是事务间因资源竞争形成的循环等待,PostgreSQL 通过 deadlock_timeout 参数控制检测频率,并通过牺牲者选择机制打破死锁。
单机环境下的行级锁交错、流复制场景中备库回放进程与用户查询的冲突、以及 UPDATE 语句因执行计划不同导致的锁获取顺序差异,都是生产环境中常见的死锁诱因。遵循“够用即可、越快越好、递增获取、相同顺序”的加锁原则,结合 log_lock_waits 日志监控与应用程序层的重试机制,是应对死锁问题的有效实践。
412



被折叠的 条评论
为什么被折叠?



