Skip to content

PostgreSQL 锁机制详解

锁的基本概念

锁的定义

锁是一种同步机制,用于防止多个事务同时修改共享资源,确保数据一致性和完整性。PostgreSQL 使用锁机制来协调并发事务,避免数据竞争和不一致。

锁的作用

  • 保证数据一致性:防止多个事务同时修改同一数据,导致数据不一致
  • 实现并发控制:允许多个事务同时访问数据库,提高系统吞吐量
  • 防止死锁:检测和处理死锁,确保系统正常运行
  • 支持事务隔离:实现不同的事务隔离级别

锁的类型

按锁粒度分类

PostgreSQL 支持多种粒度的锁:

  • 行级锁:锁定表中的单行数据,粒度最细,并发度最高
  • 页级锁:锁定数据页,粒度适中
  • 表级锁:锁定整个表,粒度最粗,并发度最低

按锁模式分类

PostgreSQL 支持多种锁模式,包括:

锁模式锁类型描述
ACCESS SHARE表级锁允许查询表,但不允许修改表结构
ROW SHARE表级锁允许并发访问表,允许行级共享锁
ROW EXCLUSIVE表级锁允许并发访问表,允许行级排他锁
SHARE UPDATE EXCLUSIVE表级锁允许并发访问表,但限制某些管理操作
SHARE表级锁允许并发读取,但不允许修改
SHARE ROW EXCLUSIVE表级锁允许并发读取,但限制某些操作
EXCLUSIVE表级锁允许并发读取,但不允许修改
ACCESS EXCLUSIVE表级锁完全排他锁,不允许任何并发访问

行级锁模式

PostgreSQL 支持以下行级锁模式:

  • FOR UPDATE:获取行级排他锁,防止其他事务修改或删除该行
  • FOR SHARE:获取行级共享锁,允许其他事务读取,但防止修改或删除
  • FOR NO KEY UPDATE:类似FOR UPDATE,但不阻塞创建唯一索引的操作
  • FOR KEY SHARE:类似FOR SHARE,但不阻塞创建唯一索引的操作

锁的获取与释放

自动获取与释放

PostgreSQL 会自动为事务获取和释放锁:

  • 获取锁:当事务执行修改操作时,自动获取相应的锁
  • 释放锁:当事务提交或回滚时,自动释放所有锁
  • 锁升级:在某些情况下,PostgreSQL 会将行级锁升级为表级锁

手动获取锁

可以使用以下语句手动获取锁:

  • LOCK TABLE:获取表级锁
  • SELECT ... FOR UPDATE:获取行级排他锁
  • SELECT ... FOR SHARE:获取行级共享锁
  • SELECT ... FOR NO KEY UPDATE:获取行级非键更新锁
  • SELECT ... FOR KEY SHARE:获取行级键共享锁

锁的兼容性

不同类型的锁之间存在兼容性关系:

  • 共享锁与共享锁:兼容,可以同时获取
  • 共享锁与排他锁:不兼容,不能同时获取
  • 排他锁与排他锁:不兼容,不能同时获取

死锁

死锁定义

死锁是指两个或多个事务相互等待对方持有的锁,导致所有事务都无法继续执行的状态。

死锁产生条件

死锁产生需要同时满足以下四个条件:

  • 互斥条件:资源只能被一个事务使用
  • 请求与保持条件:事务已持有资源,同时请求新资源
  • 不剥夺条件:资源只能由持有它的事务主动释放
  • 循环等待条件:事务之间形成循环等待链

死锁检测与处理

PostgreSQL 自动检测和处理死锁:

  • 死锁检测:定期检查等待锁的事务之间是否形成循环依赖
  • 死锁处理:选择一个事务作为牺牲品,终止并回滚该事务
  • 死锁预防:通过合理的锁获取顺序和超时机制预防死锁

锁的监控

查看当前锁状态

可以使用以下视图查看当前锁状态:

  • pg_locks:查看所有当前持有的锁
  • pg_stat_activity:查看当前活动事务和锁等待情况
  • pg_blocking_pids():查看阻塞指定进程的进程ID

锁等待日志

启用锁等待日志,可以记录锁等待事件:

  • log_lock_waits:设置为on,记录锁等待超过deadlock_timeout的事件
  • deadlock_timeout:设置锁等待超时时间,默认1秒

监控锁等待

可以使用以下查询监控锁等待:

sql
SELECT blocked_locks.pid     AS blocked_pid,
       blocked_activity.usename  AS blocked_user,
       blocking_locks.pid     AS blocking_pid,
       blocking_activity.usename AS blocking_user,
       blocked_activity.query    AS blocked_query,
       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.DATABASE IS NOT DISTINCT FROM blocked_locks.DATABASE
    AND blocking_locks.relation IS NOT DISTINCT FROM blocked_locks.relation
    AND blocking_locks.page IS NOT DISTINCT FROM blocked_locks.page
    AND blocking_locks.tuple IS NOT DISTINCT FROM blocked_locks.tuple
    AND blocking_locks.virtualxid IS NOT DISTINCT FROM blocked_locks.virtualxid
    AND blocking_locks.transactionid IS NOT DISTINCT FROM blocked_locks.transactionid
    AND blocking_locks.classid IS NOT DISTINCT FROM blocked_locks.classid
    AND blocking_locks.objid IS NOT DISTINCT FROM blocked_locks.objid
    AND blocking_locks.objsubid IS NOT DISTINCT FROM 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;

锁的优化

减少锁持有时间

  • 保持事务简短:尽量减少事务的执行时间,尽快提交或回滚
  • 批量处理:将多个小事务合并为一个大事务,减少锁获取和释放的开销
  • 合理安排语句顺序:将锁冲突较少的语句放在前面执行

选择合适的锁粒度

  • 优先使用行级锁:行级锁粒度细,并发度高
  • 避免表级锁:表级锁粒度粗,并发度低,尽量避免使用
  • 使用合适的锁模式:根据实际需求选择合适的锁模式,避免过度锁定

避免死锁

  • 合理安排事务顺序:所有事务按相同顺序访问资源
  • 设置锁超时:使用statement_timeout或lock_timeout设置锁超时
  • 避免长事务:长事务容易导致死锁,尽量保持事务简短
  • 使用乐观锁:对于读多写少的场景,考虑使用乐观锁

使用合适的事务隔离级别

  • 读已提交:默认隔离级别,适合大多数场景
  • 可重复读:适合需要一致视图的场景
  • 串行化:适合对数据一致性要求极高的场景

常见锁场景

行级锁场景

  • UPDATE/DELETE语句:自动获取行级排他锁
  • SELECT ... FOR UPDATE:手动获取行级排他锁
  • SELECT ... FOR SHARE:手动获取行级共享锁

表级锁场景

  • ALTER TABLE:获取ACCESS EXCLUSIVE锁
  • DROP TABLE:获取ACCESS EXCLUSIVE锁
  • TRUNCATE TABLE:获取ACCESS EXCLUSIVE锁
  • CREATE INDEX:获取SHARE锁(PostgreSQL 11及之前)或SHARE UPDATE EXCLUSIVE锁(PostgreSQL 12及之后)
  • VACUUM:获取SHARE UPDATE EXCLUSIVE锁

死锁场景

  • 循环等待:事务A等待事务B持有的锁,事务B等待事务A持有的锁
  • 长时间锁等待:事务持有锁时间过长,导致其他事务等待
  • 锁升级:行级锁升级为表级锁,导致大量事务等待

锁与MVCC的关系

锁与MVCC的协同工作

  • 读操作:使用MVCC读取数据,不需要获取锁,提高并发度
  • 写操作:需要获取锁,防止多个事务同时修改同一数据
  • 快照隔离:通过MVCC实现快照隔离,减少锁的使用

MVCC与锁的区别

  • MVCC:乐观并发控制,允许读写并发,写操作之间需要锁
  • :悲观并发控制,通过锁防止并发冲突
  • 协同工作:PostgreSQL结合使用MVCC和锁,实现高效的并发控制

锁的最佳实践

1. 保持事务简短

  • 原则:尽量减少事务的执行时间,尽快提交或回滚
  • 方法:将大事务拆分为多个小事务,避免长时间持有锁
  • 例外:需要保证数据一致性的操作

2. 选择合适的锁粒度

  • 原则:尽量使用行级锁,避免表级锁
  • 方法:使用SELECT ... FOR UPDATE/FOR SHARE获取行级锁,避免LOCK TABLE

3. 合理安排语句顺序

  • 原则:所有事务按相同顺序访问资源
  • 方法:先访问常用资源,后访问不常用资源;先获取弱锁,后获取强锁

4. 监控锁状态

  • 原则:定期监控锁状态,及时发现和解决锁问题
  • 方法:使用pg_locks和pg_stat_activity视图监控锁状态,启用锁等待日志

5. 避免长事务

  • 原则:尽量避免长事务,减少锁持有时间
  • 方法:将长事务拆分为多个小事务,使用批量处理

6. 使用合适的事务隔离级别

  • 原则:根据实际需求选择合适的事务隔离级别
  • 方法:默认使用读已提交,对数据一致性要求高的场景使用可重复读或串行化

常见问题(FAQ)

Q1: PostgreSQL 有哪些锁类型?

A1: PostgreSQL 支持多种锁类型,包括:

  • 按粒度分类:行级锁、页级锁、表级锁
  • 按模式分类:ACCESS SHARE、ROW SHARE、ROW EXCLUSIVE、SHARE UPDATE EXCLUSIVE、SHARE、SHARE ROW EXCLUSIVE、EXCLUSIVE、ACCESS EXCLUSIVE
  • 行级锁模式:FOR UPDATE、FOR SHARE、FOR NO KEY UPDATE、FOR KEY SHARE

Q2: 如何查看当前锁状态?

A2: 可以使用以下视图查看当前锁状态:

  • pg_locks:查看所有当前持有的锁
  • pg_stat_activity:查看当前活动事务和锁等待情况
  • pg_blocking_pids():查看阻塞指定进程的进程ID

Q3: 什么是死锁?如何避免死锁?

A3: 死锁是指两个或多个事务相互等待对方持有的锁,导致所有事务都无法继续执行。可以通过以下方式避免死锁:

  • 合理安排事务顺序,所有事务按相同顺序访问资源
  • 保持事务简短,减少锁持有时间
  • 设置锁超时,避免无限等待
  • 避免长事务,减少锁冲突

Q4: 如何优化锁性能?

A4: 可以通过以下方式优化锁性能:

  • 保持事务简短,减少锁持有时间
  • 选择合适的锁粒度,尽量使用行级锁
  • 合理安排语句顺序,避免循环等待
  • 监控锁状态,及时发现和解决锁问题
  • 使用合适的事务隔离级别

Q5: MVCC 与锁有什么区别?

A5: MVCC(多版本并发控制)是乐观并发控制,通过为每个数据行维护多个版本,允许多个事务同时读写数据,而不会相互阻塞。锁是悲观并发控制,通过锁防止并发冲突。PostgreSQL结合使用MVCC和锁,实现高效的并发控制。

Q6: 如何查看锁等待事件?

A6: 可以通过以下方式查看锁等待事件:

  • 启用log_lock_waits参数,记录锁等待超过deadlock_timeout的事件
  • 使用pg_stat_activity视图查看当前活动事务和锁等待情况
  • 使用pg_locks视图查看所有当前持有的锁

Q7: 什么情况下会产生表级锁?

A7: 以下操作会产生表级锁:

  • ALTER TABLE:获取ACCESS EXCLUSIVE锁
  • DROP TABLE:获取ACCESS EXCLUSIVE锁
  • TRUNCATE TABLE:获取ACCESS EXCLUSIVE锁
  • CREATE INDEX:获取SHARE锁或SHARE UPDATE EXCLUSIVE锁
  • VACUUM:获取SHARE UPDATE EXCLUSIVE锁

Q8: 如何避免锁升级?

A8: 可以通过以下方式避免锁升级:

  • 保持事务简短,减少锁持有时间
  • 选择合适的锁粒度,尽量使用行级锁
  • 避免一次性修改大量数据
  • 使用合适的索引,减少锁冲突

Q9: 锁等待超时怎么办?

A9: 锁等待超时可能是由于以下原因:

  • 事务持有锁时间过长
  • 死锁
  • 锁冲突

可以通过以下方式解决:

  • 终止阻塞的事务
  • 优化查询,减少锁持有时间
  • 调整事务顺序,避免死锁
  • 增加资源,提高系统吞吐量

Q10: 如何设置锁等待超时?

A10: 可以通过以下参数设置锁等待超时:

  • lock_timeout:设置语句级别的锁等待超时时间
  • statement_timeout:设置语句执行超时时间
  • deadlock_timeout:设置死锁检测超时时间

例如:

sql
SET lock_timeout = '5s';
SET statement_timeout = '30s';
SET deadlock_timeout = '1s';