libpq 流水线模式允许应用程序发送查询,而无需读取先前发送的查询的结果。利用流水线模式,客户端等待服务器的时间会更少,因为可以在单个网络事务中发送/接收多个查询/结果。
虽然流水线模式提供了显著的性能提升,但使用流水线模式编写客户端更加复杂,因为它涉及到管理待处理查询的队列,并找到哪个结果对应于队列中的哪个查询。
流水线模式通常也会消耗客户端和服务器上更多的内存,尽管小心和积极地管理发送/接收队列可以缓解这种情况。无论连接处于阻塞模式还是非阻塞模式,都是如此。
虽然 libpq 的流水线 API 在 PostgreSQL 14 中引入,但它是一个客户端特性,不需要特殊的服务器支持,并且可以在任何支持 v3 扩展查询协议的服务器上工作。有关更多信息,请参阅 第 53.2.4 节。
要发出流水线,应用程序必须将连接切换到流水线模式,这可以使用 PQenterPipelineMode
完成。PQpipelineStatus
可用于测试流水线模式是否处于活动状态。在流水线模式下,只允许使用扩展查询协议的异步操作,不允许包含多个 SQL 命令的命令字符串,也不允许使用 COPY
。使用同步命令执行函数(如 PQfn
、PQexec
、PQexecParams
、PQprepare
、PQexecPrepared
、PQdescribePrepared
、PQdescribePortal
、PQclosePrepared
、PQclosePortal
)是一种错误情况。PQsendQuery
也不允许,因为它使用简单查询协议。一旦所有已分派的命令都处理了结果,并且使用了结束流水线结果,应用程序就可以使用 PQexitPipelineMode
返回到非流水线模式。
进入流水线模式后,应用程序使用 PQsendQueryParams
或其预处理查询的同级函数 PQsendQueryPrepared
来分派请求。这些请求在客户端排队,直到刷新到服务器;当使用 PQpipelineSync
在流水线中建立同步点,或者调用 PQflush
时,就会发生这种情况。函数 PQsendPrepare
、PQsendDescribePrepared
、PQsendDescribePortal
、PQsendClosePrepared
和 PQsendClosePortal
也在流水线模式下工作。结果处理如下所述。
服务器按照客户端发送的顺序执行语句并返回结果。服务器将立即开始执行流水线中的命令,而无需等待流水线结束。请注意,结果在服务器端缓冲;当使用 PQpipelineSync
或 PQsendPipelineSync
建立同步点,或者调用 PQsendFlushRequest
时,服务器会刷新该缓冲区。如果任何语句遇到错误,服务器将中止当前事务,并且在下一个同步点之前不会执行队列中的任何后续命令;为每个此类命令生成 PGRES_PIPELINE_ABORTED
结果。(即使流水线中的命令会回滚事务,情况仍然如此。)查询处理会在同步点后恢复。
一个操作依赖于先前操作的结果是可以的;例如,一个查询可以定义一个表,同一流水线中的下一个查询会使用该表。类似地,应用程序可以创建一个命名的预处理语句,并在同一流水线中使用后续语句执行该语句。
要处理流水线中一个查询的结果,应用程序会重复调用 PQgetResult
并处理每个结果,直到 PQgetResult
返回 null。然后可以使用 PQgetResult
再次检索流水线中下一个查询的结果,并重复该循环。应用程序会像正常情况一样处理单个语句结果。当流水线中所有查询的结果都返回后,PQgetResult
将返回一个包含状态值 PGRES_PIPELINE_SYNC
的结果。
客户端可以选择延迟结果处理,直到发送完完整的流水线,或者将其与发送流水线中的其他查询交错;请参阅 第 32.5.1.4 节。
PQgetResult
的行为与正常的异步处理相同,只是它可能包含新的 PGresult
类型 PGRES_PIPELINE_SYNC
和 PGRES_PIPELINE_ABORTED
。PGRES_PIPELINE_SYNC
对于流水线中对应的每个 PQpipelineSync
或 PQsendPipelineSync
报告一次。PGRES_PIPELINE_ABORTED
将在第一次错误和直到下一个 PGRES_PIPELINE_SYNC
的所有后续结果中发出,以代替正常的查询结果;请参阅 第 32.5.1.3 节。
PQisBusy
、PQconsumeInput
等在处理流水线结果时像正常情况一样工作。特别是,如果已使用完目前为止发出的所有查询的结果,则在流水线中间调用 PQisBusy
会返回 0。
libpq 不会向应用程序提供任何关于当前正在处理的查询的信息(除了 PQgetResult
返回 null 以指示我们开始返回下一个查询的结果)。应用程序必须跟踪它发送查询的顺序,以便将查询与它们对应的结果关联起来。应用程序通常会为此使用状态机或 FIFO 队列。
从客户端的角度来看,在 PQresultStatus
返回 PGRES_FATAL_ERROR
之后,流水线会被标记为中止。PQresultStatus
将为已中止流水线中每个剩余的排队操作报告 PGRES_PIPELINE_ABORTED
结果。PQpipelineSync
或 PQsendPipelineSync
的结果报告为 PGRES_PIPELINE_SYNC
,以表示中止的流水线结束和正常结果处理的恢复。
客户端必须在错误恢复期间使用 PQgetResult
处理结果。
如果流水线使用了隐式事务,则已执行的操作会被回滚,并且已排队以跟随失败操作的操作会被完全跳过。如果流水线启动并提交单个显式事务(即,第一个语句是 BEGIN
,最后一个是 COMMIT
),则会发生相同的行为,只是会话在流水线结束时仍处于中止的事务状态。如果流水线包含多个显式事务,则在错误之前提交的所有事务仍保持提交状态,当前正在进行的事务会被中止,并且所有后续操作(包括后续事务)都会被完全跳过。如果流水线同步点发生在处于中止状态的显式事务块中,则下一个流水线将立即中止,除非下一个命令使用 ROLLBACK
将事务置于正常模式。
客户端在发送 COMMIT
时,不得假设工作已提交,只有在收到相应的确认提交完成的结果时才能假设提交完成。由于错误是异步到达的,因此如果出现问题,应用程序需要能够从最后收到的已提交更改重新启动,并重新发送该点之后完成的工作。
为了避免在大型流水线上出现死锁,客户端应围绕使用操作系统工具(如 select
、poll
、WaitForMultipleObjectEx
等)的非阻塞事件循环进行构建。
客户端应用程序通常应该维护一个待分发的工作队列,以及一个已分发但尚未处理结果的工作队列。当套接字可写时,它应该分发更多的工作。当套接字可读时,它应该读取结果并处理它们,将它们与相应的响应队列中的下一个条目匹配起来。根据可用内存,应该频繁地读取套接字中的结果:不需要等到管道结束才读取结果。管道应该限定于逻辑工作单元,通常(但不一定)每个管道一个事务。不需要在管道之间退出管道模式并重新进入,也不需要等待一个管道完成才发送下一个管道。
一个使用 select()
和一个简单的状态机来跟踪已发送和接收的工作的示例,位于 PostgreSQL 源代码发行版中的 src/test/modules/libpq_pipeline/libpq_pipeline.c
中。
PQpipelineStatus
#返回 libpq 连接的当前管道模式状态。
PGpipelineStatus PQpipelineStatus(const PGconn *conn);
PQpipelineStatus
可以返回以下值之一
PQ_PIPELINE_ON
libpq 连接处于管道模式。
PQ_PIPELINE_OFF
libpq 连接不处于管道模式。
PQ_PIPELINE_ABORTED
libpq 连接处于管道模式,并且在处理当前管道时发生错误。当 PQgetResult
返回 PGRES_PIPELINE_SYNC
类型的结果时,将清除中止标志。
PQenterPipelineMode
#如果连接当前处于空闲状态或已处于管道模式,则使其进入管道模式。
int PQenterPipelineMode(PGconn *conn);
成功时返回 1。如果连接当前未处于空闲状态,即有结果准备好,或者正在等待服务器的更多输入等,则返回 0 并且不起作用。此函数实际上不向服务器发送任何内容,它只是更改 libpq 连接状态。
PQexitPipelineMode
#如果连接当前处于管道模式,并且队列为空且没有挂起的结果,则使其退出管道模式。
int PQexitPipelineMode(PGconn *conn);
成功时返回 1。如果未处于管道模式,则返回 1 且不采取任何操作。如果当前语句未完成处理,或者 PQgetResult
尚未被调用以收集先前发送的所有查询的结果,则返回 0 (在这种情况下,请使用 PQerrorMessage
来获取有关失败的更多信息)。
PQpipelineSync
#通过发送一个 sync 消息 并刷新发送缓冲区,来标记管道中的同步点。这充当隐式事务和错误恢复点的分隔符;请参见第 32.5.1.3 节。
int PQpipelineSync(PGconn *conn);
成功时返回 1。如果连接未处于管道模式或发送 sync 消息 失败,则返回 0。
PQsendPipelineSync
#通过发送一个 sync 消息 而不刷新发送缓冲区,来标记管道中的同步点。这充当隐式事务和错误恢复点的分隔符;请参见第 32.5.1.3 节。
int PQsendPipelineSync(PGconn *conn);
成功时返回 1。如果连接未处于管道模式或发送 sync 消息 失败,则返回 0。请注意,该消息本身不会自动刷新到服务器;如有必要,请使用 PQflush
。
PQsendFlushRequest
#发送一个请求,要求服务器刷新其输出缓冲区。
int PQsendFlushRequest(PGconn *conn);
成功时返回 1。任何失败都返回 0。
服务器在调用 PQpipelineSync
的结果时,或在未处于管道模式下的任何请求时,会自动刷新其输出缓冲区;此函数可用于在管道模式下使服务器刷新其输出缓冲区,而无需建立同步点。请注意,该请求本身不会自动刷新到服务器;如有必要,请使用 PQflush
。
与异步查询模式非常相似,使用管道模式时没有明显的性能开销。它增加了客户端应用程序的复杂性,并且需要格外小心以防止客户端/服务器死锁,但是管道模式可以提供可观的性能改进,以换取由于长时间保留状态而导致的内存使用量增加。
当服务器距离较远时,即网络延迟(“ping 时间”)较高时,以及在快速连续执行许多小操作时,管道模式最有用。当每个查询的执行时间是客户端/服务器往返时间的数倍时,使用管道命令通常好处较少。在一个往返时间为 300 毫秒的服务器上运行 100 个语句的操作,仅在没有管道的情况下,网络延迟就需要 30 秒;使用管道后,它可能只需要 0.3 秒来等待服务器的结果。
当您的应用程序执行许多小的 INSERT
、UPDATE
和 DELETE
操作时,而这些操作又不能轻易地转换为对集合的操作,或转换为 COPY
操作时,请使用管道命令。
当客户端需要来自一个操作的信息来产生下一个操作时,管道模式没有用。在这种情况下,客户端将不得不引入同步点,并等待完整的客户端/服务器往返以获得所需的结果。但是,通常可以调整客户端设计以在服务器端交换所需的信息。读取-修改-写入循环尤其是不错的选择;例如
BEGIN; SELECT x FROM mytable WHERE id = 42 FOR UPDATE; -- result: x=2 -- client adds 1 to x: UPDATE mytable SET x = 3 WHERE id = 42; COMMIT;
可以使用以下方式更有效地完成:
UPDATE mytable SET x = x + 1 WHERE id = 42;
当单个管道包含多个事务时,管道化不太有用,并且更加复杂(请参见第 32.5.1.3 节)。
[15] 客户端将阻止尝试向服务器发送查询,但是服务器将阻止尝试将已处理的查询的结果发送到客户端。仅当客户端发送足够的查询以填充其输出缓冲区和服务器的接收缓冲区,然后在切换到处理来自服务器的输入之前才会发生这种情况,但是很难准确预测何时会发生这种情况。
如果您在文档中看到任何不正确的内容,与您使用特定功能的体验不符或需要进一步澄清的内容,请使用此表单报告文档问题。