postgres_lsp 锁安全规则详解:runningStatementWhileHoldingAccessExclusive 与 ACCESS EXCLUSIVE 锁泄漏检测
【免费下载链接】postgres_lspA Language Server for Postgres项目地址: https://gitcode.com/GitHub_Trending/po/postgres_lsp
本篇文章围绕 postgres_lsp 的lint/safety/runningStatementWhileHoldingAccessExclusive规则展开,系统讲解它如何检测"持有 ACCESS EXCLUSIVE 锁期间继续执行语句"这一高危模式、其底层事务状态跟踪实现,以及如何在项目中配置与规避。读完本文,你将理解该规则的工作原理、适用场景,并能把"锁内执行额外语句"这一隐患纳入日常 SQL 审查与 CI 流程。
规则概述
runningStatementWhileHoldingAccessExclusive是 postgres_lsp 中safety规则组的一员,属于lint/safety诊断类别,自vnext版本起可用。该规则已在官方文档中标记为recommended:一旦启用,lint 时若命中会直接产生诊断错误,提示开发者及时处理。
规则灵感来源于 eugene 的 E4 提示(对应源码中的RuleSource::Eugene("E4"),见 规则声明文件)。
问题背景:ACCESS EXCLUSIVE 锁的代价
在 PostgreSQL 中,ALTER TABLE等 DDL 操作会对目标表施加ACCESS EXCLUSIVE锁。这是最重的一级表级锁,它会阻塞该表上的所有并发操作,包括:
SELECT(读取也被阻塞)INSERT、UPDATE、DELETE(写入同样被阻塞)- 其他 DDL 与索引维护操作
更关键的是,锁的持有时间取决于整个事务的持续时间,而非单条语句的时长。如果开发者在同一个事务里先执行ALTER TABLE再执行其他查询,那么这些额外语句会不断延长锁的持有窗口,期间任何对该表的访问都会排队等待,严重时会造成线上服务大面积阻塞。
特别需要警惕的是,即使像SELECT COUNT(*)这样的简单查询,也可能显著拉长锁的持有时间——这正是该规则要拦截的核心场景。
触发条件与检测逻辑
什么时候会报错
规则源码(running_statement_while_holding_access_exclusive.rs)中的判断逻辑非常简洁:
let tx_state = ctx.file_context().transaction_state(); if tx_state.is_holding_access_exclusive() { diagnostics.push(LinterDiagnostic::new(...)); }即:只要当前语句执行时,文件级事务状态跟踪器显示"正在持有 ACCESS EXCLUSIVE 锁",该语句就会被标记,无论它是一条SELECT、INSERT、UPDATE还是CREATE INDEX。
规则产生的诊断消息包含三层信息:
- 主消息:"Running statement while holding ACCESS EXCLUSIVE lock."
- 详情:"This blocks all access to the table for the duration of this statement."
- 提示:"Run this statement in a separate transaction to minimize lock duration."
锁状态是如何被跟踪的
postgres_lsp 的 linter 在分析一个 SQL 文件时,会按顺序遍历每条语句,并通过AnalysedFileContext维护一个跨语句的TransactionState(见 linter_context.rs)。TransactionState专门记录"当前事务是否持有 ACCESS EXCLUSIVE 锁"(holding_access_exclusive字段),其更新逻辑(update_from_stmt)有以下关键行为:
- 锁的获取:当遇到针对已存在表的
ALTER TABLE语句,且其子命令确实需要 ACCESS EXCLUSIVE 锁时,将holding_access_exclusive置为true。 - 子命令的精细判断:并非所有
ALTER TABLE子命令都会升级到 ACCESS EXCLUSIVE 锁。例如VALIDATE CONSTRAINT实际只取SHARE UPDATE EXCLUSIVE,因此被显式排除(见is_access_exclusive_subcommand,源码中通过!matches!(subtype, AtValidateConstraint)实现)。这种精细度避免了误报。 - 新建对象的豁免:若
ALTER TABLE针对的是本事务内刚创建的表(通过has_created_object判断),不会计入锁持有——因为新建表上不存在并发读者,风险可忽略。 - 事务边界的重置:遇到
COMMIT、ROLLBACK(以及ROLLBACK TO SAVEPOINT等)时调用reset_transaction_state()清空累计状态,确保不同事务之间互不污染、不产生跨事务的误报;而BEGIN、SAVEPOINT会递增嵌套深度。
此外,同一套TransactionState还被avoid_wide_lock_window、require_idle_in_transaction_timeout、require_statement_timeout、lock_timeout_warning等 safety 规则共享,共同构成 postgres_lsp 的"事务与锁安全"检测家族。
实测用例印证
仓库自带的规则测试充分覆盖了各种触发形态(测试目录:runningStatementWhileHoldingAccessExclusive):
| 测试文件 | 场景 | 触发语句 |
|---|---|---|
basic.sql | ALTER TABLE 后执行 SELECT | SELECT COUNT(*) FROM authors; |
insert_after_alter.sql | ALTER TABLE 后执行 INSERT | INSERT INTO books (title, isbn) VALUES (...); |
create_index_after_alter.sql | ALTER TABLE 后创建索引 | CREATE INDEX orders_total_idx ON orders(total); |
multiple_statements.sql | 同事务内多条后续语句 | UPDATE、SELECT均被标记 |
对应快照basic.sql.snap记录了精确的诊断输出格式,与源码中定义的消息文案完全一致。注意multiple_statements.sql中每条后续语句都通过expect_lint标注,说明事务内 ALTER TABLE 之后的每一条语句都会独立产生一条诊断,而非只报一次。
正确用法与反例
反例(会触发诊断)
同一事务中在ALTER TABLE之后继续执行任何语句,都会触发该规则:
-- 反例 1:ALTER TABLE 后立即 SELECT ALTER TABLE authors ADD COLUMN email TEXT; SELECT COUNT(*) FROM authors; -- 反例 2:ALTER TABLE 后执行 INSERT ALTER TABLE books ADD COLUMN isbn TEXT; INSERT INTO books (title, isbn) VALUES ('Database Systems', '978-0-1234567-8-9'); -- 反例 3:ALTER TABLE 后创建索引 ALTER TABLE orders ADD COLUMN total DECIMAL(10, 2); CREATE INDEX orders_total_idx ON orders(total);正例(合规写法)
让ALTER TABLE独占一个事务,其余操作放入独立事务:
-- 单独执行 ALTER TABLE,尽快释放 ACCESS EXCLUSIVE 锁 ALTER TABLE authors ADD COLUMN email TEXT; -- 锁已释放,在后续独立事务中再执行其他查询 SELECT COUNT(*) FROM authors;如果确实需要在同一事务中执行多条 DDL,也应当优先使用并发安全写法(例如CREATE INDEX CONCURRENTLY),减少锁窗口对线上读写的影响。
如何配置该规则
配置项说明
规则在linter.rules.safety分组下,键名为runningStatementWhileHoldingAccessExclusive。它是一个标准的规则配置项,源码中对应结构体位于 linter/rules.rs,可取值包括:
"error":命中即报错(推荐,规则本身默认 recommended)"warn":命中仅告警"off":关闭规则- 或使用更细粒度的
{ "level": "warn" }对象形式
JSON 配置示例
在 postgres-language-server.jsonc 配置文件中启用:
{ "linter": { "rules": { "safety": { "runningStatementWhileHoldingAccessExclusive": "error" } } } }由于该规则默认 recommended,即使不显式配置,在默认推荐的 lint 流程中也会生效,出现诊断错误提示。你可以在 docs/reference/rules.md 中查看全部 safety 规则的索引与说明。
实战建议:把锁安全写进迁移审查
结合本规则与同组的其他锁安全规则(如require_statement_timeout、avoid_wide_lock_window、require_separate_constraint_validation),可以在提交前拦截绝大多数危险的锁模式。建议在团队中约定如下检查清单:
- 任何
ALTER TABLE语句独立成事务,事务内不混入 SELECT/INSERT/UPDATE; - 需要长时间 DDL(建索引、刷新物化视图)时优先使用
CONCURRENTLY; - 将
runningStatementWhileHoldingAccessExclusive: "error"写入共享配置,接入 CI 检查流程,让数据库迁移脚本的合并请求自动被锁安全扫描把关。
通过静态分析在代码审查阶段就发现"锁内执行语句"的隐患,比在生产环境遇到锁等待风暴再排查要高效得多——这正是该规则存在的意义。
【免费下载链接】postgres_lspA Language Server for Postgres项目地址: https://gitcode.com/GitHub_Trending/po/postgres_lsp
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考