一个打开的 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 中,将最终的事务命令纳入正常的成功和错误处理路径,以免读取完成后留下未结束的事务,导致接下来的表删除操作一直等待。

来源