Skip to content

PostgreSQL 锁和等待事件

锁类型

PostgreSQL支持多种锁类型,用于控制对数据库对象的并发访问。

1. 表级锁

锁类型描述冲突锁类型
ACCESS SHARE读取表数据(SELECT)ACCESS EXCLUSIVE
ROW SHARE读取表数据,可能修改(SELECT FOR UPDATE)EXCLUSIVE, ACCESS EXCLUSIVE
ROW EXCLUSIVE修改表数据(INSERT, UPDATE, DELETE)SHARE, SHARE ROW EXCLUSIVE, EXCLUSIVE, ACCESS EXCLUSIVE
SHARE UPDATE EXCLUSIVE并行扫描表SHARE UPDATE EXCLUSIVE, SHARE, SHARE ROW EXCLUSIVE, EXCLUSIVE, ACCESS EXCLUSIVE
SHARE创建索引(CREATE INDEX)ROW EXCLUSIVE, SHARE UPDATE EXCLUSIVE, SHARE ROW EXCLUSIVE, EXCLUSIVE, ACCESS EXCLUSIVE
SHARE ROW EXCLUSIVE行级共享锁ROW EXCLUSIVE, SHARE UPDATE EXCLUSIVE, SHARE, SHARE ROW EXCLUSIVE, EXCLUSIVE, ACCESS EXCLUSIVE
EXCLUSIVE限制表访问(ALTER TABLE ... ADD COLUMN)除ACCESS SHARE外的所有锁
ACCESS EXCLUSIVE独占表访问(DROP TABLE, TRUNCATE, VACUUM FULL)所有锁

2. 行级锁

  • FOR UPDATE:获取行的独占锁,防止其他事务修改或删除
  • FOR SHARE:获取行的共享锁,防止其他事务删除或修改
  • FOR NO KEY UPDATE:获取行的非键更新锁,防止其他事务获取FOR UPDATE锁
  • FOR KEY SHARE:获取行的键共享锁,防止其他事务获取EXCLUSIVE锁

3. 其他锁类型

  • 页级锁:保护数据页的完整性
  • 索引锁:保护索引结构的完整性
  • 扩展锁:保护表空间扩展操作
  • 事务ID锁:处理事务ID的分配和回收

等待事件

等待事件是PostgreSQL 9.6引入的功能,用于诊断事务等待的原因。

1. 锁等待事件

等待事件描述示例场景
Lock:relation等待表级锁一个事务在ALTER TABLE,另一个事务在SELECT
Lock:transactionid等待事务ID锁长事务持有锁,其他事务等待
Lock:tuple等待行级锁两个事务同时更新同一行
Lock:page等待页级锁大量并发更新同一数据页
Lock:virtualxid等待虚拟事务ID锁事务等待自身子事务完成

2. I/O等待事件

等待事件描述示例场景
IO:XactSLRULock等待SLRU页锁定大量并发事务导致SLRU页竞争
IO:BufFileRead等待临时文件读取大型排序或哈希操作
IO:BufFileWrite等待临时文件写入大型排序或哈希操作

3. 其他等待事件

等待事件描述示例场景
LWLock:buffer_content等待缓冲区内容锁大量并发访问同一缓冲区
LWLock:lock_manager等待锁管理器锁大量并发锁请求
LWLock:transactionid等待事务ID锁长事务导致事务ID回卷
Client:ClientRead等待客户端输入客户端发送请求后未响应

监控锁和等待事件

1. 查看当前锁状态

sql
-- 查看所有锁
SELECT 
  locktype, 
  database, 
  relation, 
  page, 
  tuple, 
  virtualxid, 
  transactionid, 
  pid, 
  mode, 
  granted
FROM pg_locks;

-- 查看锁与事务的关联
SELECT 
  l.locktype, 
  l.database, 
  l.relation, 
  l.mode, 
  l.granted, 
  a.pid, 
  a.usename, 
  a.application_name, 
  a.client_addr, 
  a.query, 
  a.query_start
FROM pg_locks l
JOIN pg_stat_activity a ON l.pid = a.pid;

2. 监控等待事件

sql
-- 查看当前等待事件
SELECT 
  pid, 
  usename, 
  application_name, 
  client_addr, 
  wait_event_type, 
  wait_event, 
  state, 
  query, 
  query_start, 
  now() - query_start AS duration
FROM pg_stat_activity
WHERE state = 'active' AND wait_event IS NOT NULL;

-- 统计等待事件
SELECT 
  wait_event_type, 
  wait_event, 
  count(*)
FROM pg_stat_activity
WHERE wait_event IS NOT NULL
GROUP BY wait_event_type, wait_event
ORDER BY count(*) DESC;

3. 检测死锁

sql
-- 查看死锁日志(需要配置log_deadlocks = on)
SELECT * FROM pg_log WHERE message LIKE '%deadlock%';

-- 使用pg_stat_database_conflicts查看冲突统计
SELECT * FROM pg_stat_database_conflicts;

4. 锁等待查询

sql
-- 查找阻塞其他事务的查询
SELECT 
  blocking.pid AS blocking_pid, 
  blocking.usename AS blocking_user, 
  blocking.query AS blocking_query, 
  blocking.query_start AS blocking_start, 
  blocked.pid AS blocked_pid, 
  blocked.usename AS blocked_user, 
  blocked.query AS blocked_query, 
  blocked.query_start AS blocked_start,
  now() - blocked.query_start AS wait_duration
FROM pg_stat_activity blocked
JOIN pg_stat_activity blocking ON blocked.waiting_pid = blocking.pid
WHERE blocked.waiting = true;

锁问题处理策略

1. 查找并终止阻塞事务

sql
-- 查找阻塞事务
SELECT 
  a.pid AS blocking_pid, 
  a.query AS blocking_query, 
  b.pid AS blocked_pid, 
  b.query AS blocked_query
FROM pg_locks l1
JOIN pg_stat_activity a ON l1.pid = a.pid
JOIN pg_locks l2 ON l1.locktype = l2.locktype AND l1.relation = l2.relation AND l1.page = l2.page AND l1.tuple = l2.tuple AND l1.virtualxid = l2.virtualxid AND l1.transactionid = l2.transactionid AND l1.classid = l2.classid AND l1.objid = l2.objid AND l1.objsubid = l2.objsubid
JOIN pg_stat_activity b ON l2.pid = b.pid
WHERE l1.granted AND NOT l2.granted;

-- 终止阻塞事务
SELECT pg_terminate_backend(blocking_pid);

2. 优化长事务

sql
-- 查找长事务
SELECT 
  pid, 
  usename, 
  application_name, 
  client_addr, 
  query, 
  state, 
  now() - query_start AS duration
FROM pg_stat_activity
WHERE state IN ('active', 'idle in transaction')
  AND now() - query_start > interval '5 minutes'
ORDER BY duration DESC;

-- 终止长事务
SELECT pg_terminate_backend(pid);

3. 优化锁竞争

sql
-- 减少锁持有时间
-- 1. 优化查询性能,减少事务执行时间
-- 2. 使用更小粒度的锁(行级锁而非表级锁)
-- 3. 避免长事务

-- 示例:优化批量更新,减少锁持有时间
-- 不推荐:一次性更新所有行
UPDATE users SET status = 'active';

-- 推荐:分批更新,减少锁持有时间
DO $$
DECLARE
  batch_size INT := 1000;
  total_rows INT;
  processed_rows INT := 0;
BEGIN
  SELECT COUNT(*) INTO total_rows FROM users;
  
  WHILE processed_rows < total_rows LOOP
    UPDATE users 
    SET status = 'active' 
    WHERE id IN (SELECT id FROM users ORDER BY id LIMIT batch_size OFFSET processed_rows);
    
    COMMIT;
    processed_rows := processed_rows + batch_size;
    
    -- 可选:添加延迟,减少系统负载
    -- PERFORM pg_sleep(0.1);
  END LOOP;
END $$;

配置优化

1. 锁相关配置

bash
# 配置锁等待超时(PostgreSQL 9.6+)
echo "lock_timeout = 30s" >> postgresql.conf

# 配置死锁检测时间
echo "deadlock_timeout = 1s" >> postgresql.conf

# 启用死锁日志
echo "log_deadlocks = on" >> postgresql.conf

# 记录锁等待事件
echo "log_lock_waits = on" >> postgresql.conf

# 设置日志级别
echo "log_min_messages = notice" >> postgresql.conf

# 重新加载配置
pg_ctl reload -D /var/lib/postgresql/14/main

2. 事务相关配置

bash
# 配置空闲事务超时
echo "idle_in_transaction_session_timeout = 10min" >> postgresql.conf

# 配置语句超时
echo "statement_timeout = 5min" >> postgresql.conf

# 重新加载配置
pg_ctl reload -D /var/lib/postgresql/14/main

最佳实践

1. 开发最佳实践

  • 避免长事务,尽量保持事务简短
  • 使用合适的隔离级别,避免不必要的锁
  • 使用行级锁(FOR UPDATE)代替表级锁
  • 优化查询性能,减少锁持有时间
  • 避免在事务中执行外部操作(如HTTP请求)

2. 运维最佳实践

  • 配置合理的锁等待超时和死锁检测时间
  • 启用锁等待和死锁日志
  • 监控锁等待事件,设置告警阈值
  • 定期检查长事务,及时终止异常事务
  • 使用连接池管理数据库连接,避免连接泄漏

3. 监控最佳实践

  • 使用Prometheus+Grafana监控锁和等待事件
  • 设置锁等待时间告警(如超过30秒)
  • 监控死锁发生率
  • 分析锁等待事件趋势,提前发现潜在问题
  • 定期生成锁使用报告

常见问题(FAQ)

Q1: 如何查看哪些事务正在阻塞其他事务?

A1: 使用以下SQL查询:

sql
SELECT 
  blocking.pid AS blocking_pid, 
  blocking.usename AS blocking_user, 
  blocking.query AS blocking_query, 
  blocked.pid AS blocked_pid, 
  blocked.usename AS blocked_user, 
  blocked.query AS blocked_query, 
  now() - blocked.query_start AS wait_duration
FROM pg_stat_activity blocked
JOIN pg_stat_activity blocking ON blocked.waiting_pid = blocking.pid
WHERE blocked.waiting = true;

Q2: 如何避免死锁?

A2: 可以通过以下方法避免死锁:

  • 保持事务简短,减少锁持有时间
  • 统一访问资源的顺序(如总是按主键顺序更新行)
  • 使用更低的隔离级别(如READ COMMITTED)
  • 避免在同一事务中修改多个表
  • 配置合理的锁等待超时

Q3: 如何处理锁等待超时?

A3: 处理锁等待超时的方法:

  • 分析阻塞事务,终止长时间运行的事务
  • 优化查询性能,减少锁持有时间
  • 调整锁等待超时参数(lock_timeout)
  • 考虑使用更细粒度的锁
  • 重新设计应用逻辑,避免并发冲突

Q4: 如何优化大量并发插入导致的锁竞争?

A4: 优化方法:

  • 使用分区表分散插入负载
  • 调整fillfactor参数,减少页面分裂
  • 使用批量插入代替单条插入
  • 考虑使用UNLOGGED表(如果可以接受数据丢失风险)
  • 调整wal_buffers和checkpoint参数

Q5: 如何监控锁的使用情况?

A5: 可以通过以下方式监控:

  • 查询pg_locks和pg_stat_activity视图
  • 启用log_lock_waits参数记录锁等待
  • 使用pg_stat_database_conflicts查看冲突统计
  • 结合Prometheus+Grafana设置锁等待告警
  • 使用pgBadger分析锁相关日志

Q6: 锁等待事件和死锁有什么区别?

A6:

  • 锁等待事件:一个事务等待另一个事务释放锁,可能会在超时后失败
  • 死锁:两个或多个事务互相等待对方释放锁,导致所有事务都无法继续执行
  • PostgreSQL会自动检测和解决死锁,终止其中一个事务

Q7: 如何配置锁等待日志?

A7: 需要在postgresql.conf中配置以下参数:

bash
log_lock_waits = on
log_line_prefix = '%t [%p]: [%l-1] user=%u,db=%d,app=%a,client=%h '
log_min_messages = notice

Q8: 如何优化VACUUM操作导致的锁竞争?

A8: 优化方法:

  • 使用autovacuum,避免手动VACUUM FULL
  • 调整autovacuum配置参数(如autovacuum_naptime、autovacuum_vacuum_threshold)
  • 在低峰期执行VACUUM操作
  • 使用VACUUM (FREEZE)代替VACUUM FULL
  • 考虑使用pg_repack代替VACUUM FULL