内容提要
文章讨论PostgreSQL中ALTER TABLE等模式变更的锁等待问题:即使语句本身很快,若等待ACCESS EXCLUSIVE锁,会阻塞后续所有读取。关键在于锁的获取时间、语句持续时间和事务持有时间。建议设置lock_timeout快速失败,并用NOT VALID约束或CREATE INDEX CONCURRENTLY等弱锁操作,避免长时间阻塞。
延伸解读
锁等待的连锁效应
文章指出,一个等待ACCESS EXCLUSIVE锁的ALTER TABLE语句,即使自身执行很快,也会阻塞后续所有读取操作,即使这些读取与已持有的锁兼容。这是因为PostgreSQL的锁请求排队机制:一旦有冲突的锁请求在等待,后续请求即使兼容也会被阻塞。因此,一个长时间运行的查询可能导致后续所有读操作被卡住,直到该事务结束。
区分三种时间:获取、执行与持有
文章强调,评估迁移影响需区分三个时间:锁获取时间(受lock_timeout限制)、语句执行时间(受statement_timeout限制)和锁持有时间(直到事务结束)。许多“安全迁移”建议只关注前两者,但锁持有时间才是决定阻塞时长的关键。一个在事务开头执行的快速ALTER TABLE,如果事务持续五分钟,就会阻塞五分钟。
弱锁操作的实际应用
文章提供了几种避免长时间阻塞的弱锁操作:ADD CHECK ... NOT VALID仅需短暂ACCESS EXCLUSIVE,VALIDATE CONSTRAINT使用SHARE UPDATE EXCLUSIVE,CREATE INDEX CONCURRENTLY不阻塞读写。但需注意,CREATE INDEX CONCURRENTLY不能在事务块或函数中执行,且ADD FOREIGN KEY虽不阻塞读,但会阻塞写。
lock_timeout的陷阱与正确设置
设置lock_timeout可以快速失败,避免长时间阻塞队列。但需注意:lock_timeout必须小于非零的statement_timeout才有效;单位是毫秒;deadlock_timeout只调度死锁检查,不会中止语句。此外,lock_timeout只限制单次锁获取,若语句需多个锁,总等待时间可能超过该值。
Q&A
为什么一个执行很快的ALTER TABLE语句会导致后续的SELECT查询被阻塞很长时间?
因为ALTER TABLE需要获取ACCESS EXCLUSIVE锁,如果该锁被其他事务持有,ALTER TABLE会等待。在等待期间,后续的SELECT请求(即使与当前持有锁的事务兼容)也会被阻塞,因为等待中的ACCESS EXCLUSIVE锁请求会阻止后续的锁请求排队。
PostgreSQL中ALTER TABLE操作默认获取什么锁?哪些操作会获取较弱的锁?
默认情况下,ALTER TABLE获取ACCESS EXCLUSIVE锁,这会阻塞所有其他操作。但某些子操作会使用较弱的锁,例如:ADD CHECK ... NOT VALID和ADD COLUMN(非易变默认值)仅需短暂持有ACCESS EXCLUSIVE;VALIDATE CONSTRAINT使用SHARE UPDATE EXCLUSIVE;ADD FOREIGN KEY使用SHARE ROW EXCLUSIVE;CREATE INDEX CONCURRENTLY使用SHARE UPDATE EXCLUSIVE。
如何避免ALTER TABLE长时间阻塞读写?有哪些具体方法?
两种主要方法:1) 设置lock_timeout,让等待锁的语句快速失败,避免长时间阻塞队列;2) 拆分操作,将耗时部分(如扫描)放在弱锁下执行,例如使用NOT VALID约束和VALIDATE CONSTRAINT,或使用CREATE INDEX CONCURRENTLY。
lock_timeout和statement_timeout有什么区别?设置时需要注意什么?
lock_timeout限制语句等待锁的时间,而statement_timeout限制整个语句的执行时间。lock_timeout必须小于非零的statement_timeout才能生效,且单位是毫秒。另外,lock_timeout只影响锁等待,不会中止已获取锁后的执行。
为什么在事务中执行ALTER TABLE即使语句很快也可能导致长时间阻塞?
因为PostgreSQL中的表级锁会一直持有到事务结束,而不是语句结束。如果ALTER TABLE在一个长事务中执行,即使语句本身很快,锁也会被持有到事务提交或回滚,从而长时间阻塞其他操作。
CREATE INDEX CONCURRENTLY有什么限制?为什么不能在事务块中执行?
CREATE INDEX CONCURRENTLY不能在事务块中执行,也不能在函数或多语句查询中执行。这是因为并发构建索引需要长时间持有SHARE UPDATE EXCLUSIVE锁,如果放在事务中,会延长锁的持有时间,违背其设计初衷。
如何安全地给列添加NOT NULL约束而不长时间阻塞读写?
可以使用四步法:1) 添加NOT VALID的CHECK约束;2) 使用VALIDATE CONSTRAINT验证,该操作使用SHARE UPDATE EXCLUSIVE锁,允许读写;3) 添加NOT NULL约束,此时由于CHECK约束已验证,不会扫描表;4) 删除冗余的CHECK约束。