libpq 管道模式允许应用发送查询而无需读取先前发送查询的结果。 利用管道模式的优点,客户端将对服务器等待的更少,因为可以在单个网络事务中发送/接收多个查询/结果。
虽然管道模式提供了显著的性能提升, 使用管道模式写客户端会更加复杂,因为它涉及到管理一个挂起查询的队列,并查找队列中的哪个结果对应于哪个查询。
管道模式通常也会在客户端和服务器上面消耗更多的内存,尽管仔细和积极地管理发送/接收队列可以减轻这种消耗。 这适用于连接是否处于阻塞或非阻塞模式。
虽然libpq的流水线API是在PostgreSQL 14中引入的, 但它是一个客户端特性,不需要特殊的服务器支持,并且适用于支持v3扩展查询协议的任何服务器。 欲了解更多信息,请参阅第 55.2.4 节。
要发出管道命令,应用程序必须将连接切换到管道模式,
可以通过PQenterPipelineMode
来完成。
可以使用PQpipelineStatus
来测试管道模式是否激活。
在管道模式下,只允许使用扩展查询协议的异步操作,
不允许包含多个SQL命令的命令字符串,也不允许使用COPY
命令。
使用同步命令执行函数,如PQfn
、
PQexec
、
PQexecParams
、
PQprepare
、
PQexecPrepared
、
PQdescribePrepared
、
PQdescribePortal
,
都会导致错误。
也不允许使用PQsendQuery
,因为它使用简单查询协议。
一旦所有已分派的命令的结果已被处理,并且
结束管道结果已被消耗,应用程序可以通过PQexitPipelineMode
返回
到非管道模式。
最好在libpq在non-blocking mode的时候使用管道模式。 如果在阻塞模式下使用,它可能发生客户端/服务器死锁。 [15]
进入管道模式后,应用程序使用PQsendQueryParams
或其准备查询的兄弟节点PQsendQueryPrepared
来分派请求。
这些请求在客户端排队,直到刷新到服务器端;当使用PQpipelineSync
建立管道中的同步点时,或者调用PQflush
时,会发生这种情况。
函数PQsendPrepare
、PQsendDescribePrepared
和PQsendDescribePortal
在管道模式下也可以工作。
结果处理如下所述。
服务器执行语句,并返回结果,按客户端发送它们的顺序。
服务器将立即开始执行管道中的命令,不等待管道的结束。
注意,结果被缓冲在服务器端端;当使用PQpipelineSync
建立同步点时,或者PQsendFlushRequest
被调用时,服务器会刷新该缓冲区。
如果任何语句遇到错误,服务器将中止当前事务,并且不执行队列中的任何后续命令,直到下一个同步点;对于每一个这样的命令都会产生一个PGRES_PIPELINE_ABORTED
结果。
(即使管道中的命令将回滚事务,依然如此。)
查询处理在同步点之后恢复。
一个操作依赖于前一个操作的结果是没问题的; 例如,一个查询可以定义一个同一管道中的下一个查询将用到的表。 类似地,应用可以创建一个命名的预处理语句,并在同一管道中与后续语句一起执行它。
为了在管道中处理一个查询的结果,应用重复调用PQgetResult
并处理每个结果,直到PQgetResult
返回空。
可以再次使用PQgetResult
检索管道中下一个查询的结果,并且循环重复
应用像通常一样处理单个语句结果。
当管道中所有查询的结果都返回时,PQgetResult
返回一个结果,其包含状态值PGRES_PIPELINE_SYNC
。
客户端可以选择延迟结果处理,直到完整的管道被发送,或者与管道中发送的更多的查询交错在一起;参见第 34.5.1.4 节。
要进入单行模式,在使用PQgetResult
检索结果之前调用PQsetSingleRowMode
。
此模式选择仅对当前处理的查询有效。
有关使用PQsetSingleRowMode
的更多信息,请参阅第 34.6 节。
PQgetResult
行为与普通异步处理相同,除了它可能包含新的PGresult
类型PGRES_PIPELINE_SYNC
和PGRES_PIPELINE_ABORTED
。
对于每个在管道中相应的点的PQpipelineSync
,PGRES_PIPELINE_SYNC
确定报告一次。
PGRES_PIPELINE_ABORTED
在第一个错误和所有后续结果的正常查询结果中发出,直到下一个PGRES_PIPELINE_SYNC
;参见第 34.5.1.3 节。
PQisBusy
、PQconsumeInput
、等正常操作,在处理管道结果时。
特别是,在管道中间调用PQisBusy
时,如果到目前为止发出的所有查询的结果都已被消耗,则返回0。
libpq不向应用提供有关当前正在处理的查询的任何信息(除了PQgetResult
返回空,以表示我们开始返回下一个查询的结果)。
应用必须保持跟踪它发送查询的顺序,以将它们与相应的结果相关联。
应用通常会为此使用状态机或FIFO队列。
从客户端的视角,在PQresultStatus
返回PGRES_FATAL_ERROR
之后,管道被标记为中止。
PQresultStatus
将报告一个PGRES_PIPELINE_ABORTED
结果,对中止的管道中的每一个剩余的排队操作。
PQpipelineSync
的结果报告为PGRES_PIPELINE_SYNC
,以表示中止管道的结束并恢复正常的结果处理。
在错误恢复时,客户端必须 使用 PQgetResult
处理结果。
如果管道使用隐式事务,那么已经执行的操作将被回滚,而排队跟在失败操作的操作将被完全跳过。
同样的行为也会发生,如果管道开始并提交单个显式事务(也就是,第一个语句是BEGIN
,最后一个是COMMIT
),除非会话在管道结束时保持在中止事务状态。
如果管道包含multiple explicit transactions,所有错误之前提交的事务会继续提交,当前正在进行的事务被中止,所有后续操作被完全跳过,包括后续的事务。
如果管道同步点发生时显式事务块为中止状态,则下一个管道将立即中止,除非下一个命令使用ROLLBACK
将事务置于正常模式。
客户端绝对不可以假设工作已经被提交,当它sends一个COMMIT
—只有相应的结果被接收时,才能确认提交已完成。
因为错误是异步到达的,如果出现错误,应用需要能够从最后一个received到的提交的更改重新开始,并重新发送在该点之后完成的工作。
为避免大型管道上的死锁,客户端将被围绕非阻塞事件循环构建,通过使用操作系统工具,如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 message并刷新发送缓冲区的方式来标记管道中的同步点。 它用作隐式事务的定界符和错误恢复点;参见第 34.5.1.3 节。
int PQpipelineSync(PGconn *conn);
成功就返回1。 如果连接不是管道模式或者发送sync message失败,则返回0。
PQsendFlushRequest
发送一个请求到服务器以刷新它的输出缓冲区。
int PQsendFlushRequest(PGconn *conn);
成功就返回1。 任何失败则返回0。
当PQpipelineSync
被调用的结果,或者在非管道模式下的任何请求时,服务器自动刷新它的输出缓冲区;这个函数用于使得服务器在不建立同步点的情况下以管道模式刷新其输出缓冲区。
请注意,请求不是自身能自动刷新到服务器;如果需要时使用PQflush
。
非常像异步查询模式,在使用管道模式时没有明显的性能开销。 它增加了客户端应用的复杂性,并且需要特别注意以防止客户端/服务器死锁。 但是管道模式可以提供相当大的性能改进,以换取内存使用率的增加,从离开撞到到更久。
当服务器比较远,即网络延迟(“ping time”)高的时候,管道模式最有用,以及在许多小操作正在快速连续执行的时候。 当每个查询的执行时间是客户端/服务器往返时间的许多倍时,使用管道命令的优势通常会更少。 不用管道模式,在往返时间为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;
could be much more efficiently done with:
UPDATE mytable SET x = x + 1 WHERE id = 42;
当单个管道包含多个事务时,管道的作用更小,而且更复杂(参见第 34.5.1.3 节)。
[15] 客户端将阻止尝试发送查询到服务器,但服务器将阻止从它已经处理的查询向客户端发送结果。 这只有当客户端在它切换到处理从服务器的输入之前发送足够的查询来填充其输出缓冲区和服务器的接收缓冲区,才会发生这种情况,但很难准确预测何时将发生。