一个打开的 PostgreSQL 只读事务可能会阻止 DROP TABLE,因为其中的 SELECT 会持有 ACCESS SHARE 表锁,直到事务结束。删除操作需要与之冲突的 ACCESS EXCLUSIVE 锁,因此会等待——即使查询已经返回也是如此。PostgreSQL:显式锁定
本指南介绍如何在 Mac 上重现这种等待、找出阻塞会话,并在 node-postgres 中正确结束事务。指南只使用合成文章行。SQL 面向 PostgreSQL 18,这是本文检索官方当前文档时所对应的版本;应用特定版本的设置前,请查阅与你的服务器版本相匹配的手册。
PostgreSQL 锁中的只读是什么意思?
READ ONLY 限制事务可以执行的命令。例如,它不允许修改普通表,也不允许执行 DROP TABLE 等命令。但它不会让读取操作免于表锁。在此场景中,读取方与尝试删除表的会话属于不同事务。PostgreSQL:SET TRANSACTION
普通的 SELECT 会自动对所引用的表获取 ACCESS SHARE 锁。DROP TABLE 需要 ACCESS EXCLUSIVE 锁,而该锁与之冲突。锁通常会一直持有到事务结束;有一个文档中说明的例外:回滚到获取锁之前建立的保存点。以下示例不使用保存点。PostgreSQL:显式锁定
关键在于区分查询完成和事务完成。在终端中看到文章行,只能说明查询已返回。还要检查其所在事务是否也已结束。在 psql 中,通常会启用自动提交,但显式的 BEGIN 会让事务保持打开状态,直到你将其结束。PostgreSQL:psql
如何在 Mac 上重现被阻塞的表删除?
使用已有的本地 PostgreSQL 18 测试数据库,并确保 Mac 的终端中已经可以使用 psql。打开三个连接到同一数据库的终端标签页,分别称为 A、B 和 C。这些必须是不同的连接;仅仅使用不同的编辑器窗格并不能保证这一点。
以下连接命令假定你的本地测试数据库名为 postgres。如有需要,请替换为你已有的测试数据库名称。每个标签页都运行此命令。-X 选项可避免加载个人的 psql 启动文件。PostgreSQL:psql
psql -X -d postgres
在每个连接中,显式启用自动提交并检查服务器版本。以下代码块按顺序构成操作流程:后续代码块依赖前面建立的设置和会话状态。
\set AUTOCOMMIT on
SHOW server_version;
创建合成文章表
在标签页 C 中运行一次设置代码。如果 public.article_lock_demo 已存在,请停止并选择另一个名称,并在所有示例中统一使用该名称。此设置有意不会删除现有表。
CREATE TABLE public.article_lock_demo (
id integer PRIMARY KEY,
title text NOT NULL
);
INSERT INTO public.article_lock_demo (id, title)
VALUES
(1, 'Choosing a notebook'),
(2, 'Organizing a reading list');
让读取事务保持打开
在标签页 A 中运行此代码块,并让连接停留在提示符处。暂时不要提交。
SET application_name = 'article_reader_demo';
BEGIN READ ONLY;
SHOW transaction_read_only;
SELECT id, title
FROM public.article_lock_demo
ORDER BY id;
预期 transaction_read_only 会显示 on,随后是两条合成行。读取已完成,但显式事务仍然打开。
从另一个连接尝试删除表
在标签页 B 中运行以下代码块。将 DROP TABLE 作为最后一条语句提交,不要在后面追加其他命令。
SET application_name = 'article_drop_demo';
BEGIN;
SET LOCAL lock_timeout = '60s';
DROP TABLE public.article_lock_demo;
只要 A 仍保持打开,删除操作就应该会等待。示例中的 60 秒上限让你有时间检查等待状态;这不是适用于所有场景的推荐设置。lock_timeout 会中止等待锁的时间过长的语句,并且会分别应用于每次锁获取尝试。PostgreSQL:客户端连接默认设置
删除操作返回后,让 B 的显式事务继续保持打开。稍后你会回滚该事务,这样即使删除成功,演示表也能保留下来。提交其事务时,DROP TABLE 会删除表及其关联的索引和约束。PostgreSQL:DROP TABLE
如何确认是哪个会话在阻塞删除操作?
B 等待期间,在 C 中运行下一个查询。它会找出两个演示连接,并让 PostgreSQL 查询哪些会话正在阻塞各连接的锁请求。进程未被阻塞时,pg_blocking_pids() 会返回空数组;被阻塞时,则会返回阻塞进程 ID 的数组。PostgreSQL:系统信息函数和运算符
SELECT
pid,
application_name,
state,
xact_start,
wait_event_type,
wait_event,
pg_blocking_pids(pid) AS blocking_pids,
query
FROM pg_stat_activity
WHERE datname = current_database()
AND application_name IN (
'article_reader_demo',
'article_drop_demo'
)
ORDER BY application_name;
预期 A 显示 idle in transaction:它有一个打开的事务,但当前没有执行查询。等待时,B 可能显示 active,等待事件类型为 Lock。PostgreSQL 文档说明,执行状态和等待事件彼此独立,因此仅凭 active 并不能确定命令正在取得进展。PostgreSQL:累积统计信息系统
接下来检查实际的表锁请求。在结束 A 的事务之前,在 C 中运行此查询。
SELECT
a.application_name,
l.pid,
l.mode,
l.granted
FROM pg_locks AS l
JOIN pg_stat_activity AS a ON a.pid = l.pid
WHERE l.locktype = 'relation'
AND l.database = (
SELECT oid
FROM pg_database
WHERE datname = current_database()
)
AND l.relation = 'public.article_lock_demo'::regclass
ORDER BY a.application_name, l.mode;
以下预期模式仅作示例,并非实际执行输出。在 pg_locks 中,granted = true 表示进程持有该锁;false 表示进程正在等待该锁。PostgreSQL:pg_locks
| 会话 | 锁模式 | granted |
含义 |
|---|---|---|---|
article_reader_demo |
AccessShareLock |
true |
读取方持有其表锁 |
article_drop_demo |
AccessExclusiveLock |
false |
删除操作正在等待其表锁 |
按顺序结合使用这两个查询进行诊断:先确认阻塞关系,再检查相关表上已持有的锁和待获取的锁。仅凭会话处于空闲状态不足以作为证据;阻塞关系和锁记录能够将其与这次具体的删除操作联系起来。
如果 B 已经超时,其等待中的锁请求将不再显示。在 B 中运行 ROLLBACK;,然后重新运行 B 的事务代码块;只要 A 仍保持打开,就及时检查。
如何释放读取方并完成演示?
在 A 中结束只读事务:
COMMIT;
如果 B 仍在等待,且 A 是唯一的阻塞方,那么删除操作现在可以完成。在 B 中立即撤销这次演示用的删除操作,保留合成表以供后续使用:
ROLLBACK;
如果 B 在你提交 A 之前就已超时,请先在 B 中运行 ROLLBACK;。A 结束后,重新运行 B 的代码块;删除成功后,再次运行 ROLLBACK;。这样无论出现哪种结果,都不会让 B 的事务处于未完成状态。
以下决策参考有助于区分事务生命周期问题和其他原因。会话状态名称遵循 PostgreSQL 文档中的定义。PostgreSQL:累积统计信息系统
| 观察结果 | 下一步操作 |
|---|---|
A 为 idle in transaction,且 B 的阻塞方数组包含 A 的 PID |
在 A 的连接中结束 A 的事务 |
| A 已结束,但 B 仍报告阻塞 PID | 检查其余阻塞方 |
| B 报告锁超时 | 重试之前回滚 B 的显式事务 |
| B 返回依赖项错误 | 检查依赖项,不要将其视为锁等待 |
| B 立即返回 | 检查预期的读取事务是否仍然打开 |
IF EXISTS 用于处理表不存在的情况,而 CASCADE 控制是否删除依赖对象。两者都不是用于处理锁等待的选项。本练习保留默认的依赖项行为;官方 DROP TABLE 语法没有 CONCURRENTLY 变体。PostgreSQL:DROP TABLE
node-postgres 应如何结束只读事务?
使用 node-postgres 时,对 BEGIN、事务中的每条语句以及最后的 COMMIT 或 ROLLBACK,都应使用同一个签出的客户端。官方事务文档明确警告:事务中不要使用 pool.query(),因为语句可能会被分派到不同的客户端。node-postgres:事务
将事务结束和释放客户端视为应用中的两个独立步骤。client.release() 会将签出的客户端归还给连接池;调用它之前,应先结束事务。如果回滚失败,连接池 API 支持通过向 release() 传入真值来销毁客户端。node-postgres:连接池 API
以下完整程序会保留操作流程中的合成表。请使用支持 ES 模块和 AggregateError 的已安装 Node.js 运行时,并在项目工作目录中安装 pg:
npm install pg
将以下内容保存为 read-articles.mjs。将其连接配置为与 psql 相同的本地测试数据库。node-postgres 支持 PostgreSQL 连接环境变量,包括 PGDATABASE。node-postgres:连接
import pg from 'pg';
const { Pool } = pg;
const pool = new Pool({ connectionTimeoutMillis: 5000 });
async function main() {
try {
const client = await pool.connect();
let discard = false;
try {
await client.query('BEGIN READ ONLY');
const result = await client.query(
'SELECT id, title FROM public.article_lock_demo ORDER BY id'
);
await client.query('COMMIT');
console.table(result.rows);
} catch (error) {
try {
await client.query('ROLLBACK');
} catch (rollbackError) {
discard = true;
throw new AggregateError(
[error, rollbackError],
'Query and rollback failed'
);
}
throw error;
} finally {
client.release(discard);
}
} finally {
await pool.end();
}
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
PGDATABASE=postgres node read-articles.mjs
程序会先提交,再显示行;发生错误时会回滚,并在任何情况下释放客户端。其 JavaScript 语法已使用 Node.js 检查;由于研究环境中没有 PostgreSQL,因此这里只根据文档检查了数据库操作流程,并未实际运行。
对于单次独立读取,pool.query() 是文档所述的便捷方式,无需手动签出客户端。当任务需要显式事务时,请使用显式事务,并将无关的文件处理或桌面交互安排在事务生命周期之外。node-postgres:连接池 API
哪种超时设置可以处理未结束的读取事务?
根据等待的类型选择超时设置。lock_timeout 限制删除操作获取锁时的等待时间。statement_timeout 限制语句执行时间,因此即使应用在读取成功后停止发送命令,它也不会结束事务。idle_in_transaction_session_timeout 专门用于终止那些在打开事务中处于空闲状态的会话。这些都是 PostgreSQL 18 的文档设置;请根据应用的预期工作时长选择持续时间。PostgreSQL:客户端连接默认设置
对于这项任务,关键在于确认读取方的事务是否已结束。确认阻塞 PID,检查表锁,并在持有该事务的连接上结束事务。在 node-postgres 中,将最终的事务命令纳入正常的成功和错误处理路径,以免读取完成后留下未结束的事务,导致接下来的表删除操作一直等待。
来源
- 13.3. 显式锁定 — PostgreSQL 全球开发组
- SET TRANSACTION — PostgreSQL 全球开发组
- psql — PostgreSQL 全球开发组
- 19.11. 客户端连接默认设置 — PostgreSQL 全球开发组
- DROP TABLE — PostgreSQL 全球开发组
- 9.27. 系统信息函数和运算符 — PostgreSQL 全球开发组
- 27.2. 累积统计信息系统 — PostgreSQL 全球开发组
- 53.13. pg_locks — PostgreSQL 全球开发组
- 事务 — node-postgres
- 连接池 API — node-postgres
- 连接 — node-postgres