外观
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';