1. PostgreSQL锁机制基础解析
PostgreSQL作为一款企业级开源关系数据库,其锁机制设计直接影响着并发性能和数据一致性。与MySQL等数据库不同,PostgreSQL实现了真正的多版本并发控制(MVCC),这使得"读不阻塞写,写不阻塞读"成为可能。但涉及到表结构变更时,特殊的锁冲突问题就会显现。
PostgreSQL的表级锁共有八个级别,按冲突程度从低到高依次为:
- AccessShare(访问共享锁)
- RowShare(行共享锁)
- RowExclusive(行排他锁)
- ShareUpdateExclusive(共享更新排他锁)
- Share(共享锁)
- ShareRowExclusive(共享行排他锁)
- Exclusive(排他锁)
- AccessExclusive(访问排他锁)
每个锁模式都有特定的应用场景和冲突规则。例如,普通的SELECT查询会获取AccessShare锁,而UPDATE/DELETE/INSERT等DML操作会获取RowExclusive锁。这两种锁之间不会产生冲突,这也是PostgreSQL实现读写不阻塞的基础。
2. ALTER TABLE的锁获取机制
ALTER TABLE命令在执行时会根据操作类型获取不同级别的锁。常见的几种情况:
- 添加列(ADD COLUMN):需要AccessExclusive锁
- 修改列类型(ALTER COLUMN TYPE):需要AccessExclusive锁
- 添加约束(ADD CONSTRAINT):可能需要ShareUpdateExclusive或AccessExclusive锁
- 设置默认值(SET DEFAULT):只需要ShareUpdateExclusive锁
- 验证约束(VALIDATE CONSTRAINT):只需要ShareUpdateExclusive锁
AccessExclusive锁是最高级别的锁,与所有其他锁模式都冲突。这意味着当ALTER TABLE需要这种锁时,它会阻塞所有其他访问该表的操作,包括普通的SELECT查询。
3. 阻塞场景分析与重现
让我们通过实际案例来演示ALTER TABLE的阻塞问题:
3.1 基础阻塞场景
会话1:
sql复制BEGIN;
SELECT * FROM users WHERE id = 1; -- 获取AccessShare锁
会话2:
sql复制ALTER TABLE users ADD COLUMN phone VARCHAR(20); -- 尝试获取AccessExclusive锁
此时会话2会被阻塞,因为AccessShare锁与AccessExclusive锁冲突。可以通过以下查询查看锁等待情况:
sql复制SELECT blocked_locks.pid AS blocked_pid,
blocking_locks.pid AS blocking_pid,
blocked_activity.usename AS blocked_user,
blocking_activity.usename AS blocking_user,
blocked_activity.query AS blocked_statement,
blocking_activity.query AS blocking_statement
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;
3.2 长事务导致的阻塞
更隐蔽的问题是长事务导致的ALTER TABLE阻塞:
sql复制-- 会话1
BEGIN;
SELECT * FROM users WHERE id = 1;
-- 不提交,保持事务打开
-- 会话2(几分钟甚至几小时后)
ALTER TABLE users ADD COLUMN phone VARCHAR(20); -- 被阻塞
这种情况下,DBA可能很难立即发现阻塞源头,因为最初的SELECT查询可能已经执行很久了。
4. 解决方案与最佳实践
4.1 避免阻塞的策略
-
使用低锁级别操作:尽可能使用不需要AccessExclusive锁的ALTER TABLE操作。例如:
- 添加不带默认值的可为NULL列(PG11+)
- 使用ADD CONSTRAINT ... NOT VALID加上VALIDATE CONSTRAINT组合
-
锁超时设置:
sql复制SET lock_timeout = '5s'; -- 设置锁等待超时 ALTER TABLE users ADD COLUMN phone VARCHAR(20); -
在维护窗口执行:安排DDL变更在低峰期进行,减少影响。
-
使用pg_repack扩展:这个工具可以在线重建表,避免长时间锁表。
4.2 监控与诊断
-
使用pg_stat_activity和pg_locks视图监控阻塞情况:
sql复制SELECT pid, wait_event_type, wait_event, query FROM pg_stat_activity WHERE wait_event_type IS NOT NULL; -
设置告警规则,当发现AccessExclusive锁等待超过阈值时触发通知。
-
使用pg_blocking_pids()函数快速查找阻塞源:
sql复制SELECT pg_blocking_pids(pid) FROM pg_stat_activity WHERE pid = <被阻塞的PID>;
4.3 PostgreSQL版本差异
不同版本的PostgreSQL在ALTER TABLE锁行为上有所改进:
- PG11:ADD COLUMN WITH DEFAULT值不再需要重写表
- PG12:ALTER TABLE某些操作性能提升
- PG14:ALTER TABLE...DETACH PARTITION并发性改进
5. 高级技巧与内部原理
5.1 理解锁获取过程
当执行ALTER TABLE时,PostgreSQL会:
- 检查语法和权限
- 获取目标表的AccessExclusive锁
- 执行元数据变更
- 必要时重写表数据(如修改列类型)
- 更新系统目录
- 释放锁
5.2 避免锁问题的设计模式
- 使用应用层合并列:考虑使用JSONB字段替代频繁的加列操作
- 采用扩展表设计:将不常用字段放在关联表中
- 使用分区表:在特定分区上执行DDL而非整表
5.3 特殊场景处理
-
在热备服务器上执行ALTER TABLE:
sql复制ALTER TABLE ... WITH (allow_system_table_mods = true); -
在事务中批量执行DDL:
sql复制BEGIN; LOCK TABLE users IN ACCESS EXCLUSIVE MODE; ALTER TABLE users ADD COLUMN phone VARCHAR(20); ALTER TABLE users ADD COLUMN address TEXT; COMMIT;
6. 性能优化建议
-
对大表执行ALTER TABLE前:
- 考虑使用pg_repack
- 在测试环境评估执行时间
- 准备回滚方案
-
使用并发索引创建:
sql复制CREATE INDEX CONCURRENTLY ON users(email); -
对于必须的长时间DDL操作,考虑:
- 使用在线schema变更工具
- 实现蓝绿部署方案
- 使用逻辑复制构建新表结构
7. 真实案例:电商平台用户表扩展
某电商平台需要在users表添加会员等级字段,表数据量5TB,QPS 3000+。解决方案:
- 创建新表users_new包含新字段
- 设置逻辑复制从users到users_new
- 应用双写
- 验证数据一致性
- 切换表名
这种方法实现了零停机schema变更,整个过程对应用完全透明。
