libpq 管道模式允许应用发送查询而无需读取先前发送查询的结果。 利用管道模式的优点,客户端将对服务器等待的更少,因为可以在单个网络事务中发送/接收多个查询/结果。
虽然管道模式提供了显著的性能提升, 使用管道模式写客户端会更加复杂,因为它涉及到管理一个挂起查询的队列,并查找队列中的哪个结果对应于哪个查询。
管道模式通常也会在客户端和服务器上面消耗更多的内存,尽管仔细和积极地管理发送/接收队列可以减轻这种消耗。 这适用于连接是否处于阻塞或非阻塞模式。
虽然libpq的流水线API是在PostgreSQL 14中引入的, 但它是一个客户端特性,不需要特殊的服务器支持,并且适用于支持v3扩展查询协议的任何服务器。 欲了解更多信息,请参阅Section 53.2.4。
要发出流水线命令,应用程序必须将连接切换到流水线模式,
这可以通过PQenterPipelineMode
完成。
PQpipelineStatus
可用于测试流水线模式是否激活。
在流水线模式下,只允许使用扩展查询协议的
异步操作,
不允许包含多个SQL命令的命令字符串,也不允许使用COPY
。
使用同步命令执行函数
如PQfn
、
PQexec
、
PQexecParams
、
PQprepare
、
PQexecPrepared
、
PQdescribePrepared
、
PQdescribePortal
、
PQclosePrepared
、
PQclosePortal
,
都属于错误操作。
PQsendQuery
也不允许,因为它使用简单查询协议。
一旦所有派发的命令结果都已处理完毕,并且流水线结束结果已被消费,
应用程序可以通过PQexitPipelineMode
返回非流水线模式。
最好在libpq在non-blocking mode的时候使用管道模式。 如果在阻塞模式下使用,它可能发生客户端/服务器死锁。 [15]
进入流水线模式后,应用程序使用
PQsendQueryParams
或其预处理查询对应函数
PQsendQueryPrepared
来分发请求。
这些请求会在客户端排队,直到刷新到服务器;
这发生在使用 PQpipelineSync
在流水线中建立同步点时,或者调用 PQflush
时。
函数 PQsendPrepare
、
PQsendDescribePrepared
、
PQsendDescribePortal
、
PQsendClosePrepared
和
PQsendClosePortal
也支持流水线模式。
结果处理将在下文描述。
服务器按照客户端发送的顺序执行语句并返回结果。服务器会立即开始执行
管道中的命令,而不会等待管道结束。请注意,结果会在服务器端缓冲;
当通过PQpipelineSync
或
PQsendPipelineSync
建立同步点,或者调用
PQsendFlushRequest
时,服务器会刷新该缓冲区。
如果任何语句遇到错误,服务器会中止当前事务,并且在下一个同步点之前
不执行队列中的后续命令;每个此类命令都会产生一个PGRES_PIPELINE_ABORTED
结果。
(即使管道中的命令会回滚事务,这一点仍然成立。)
查询处理将在同步点之后恢复。
一个操作依赖于前一个操作的结果是没问题的; 例如,一个查询可以定义一个同一管道中的下一个查询将用到的表。 类似地,应用可以创建一个命名的预处理语句,并在同一管道中与后续语句一起执行它。
为了在管道中处理一个查询的结果,应用重复调用PQgetResult
并处理每个结果,直到PQgetResult
返回空。
可以再次使用PQgetResult
检索管道中下一个查询的结果,并且循环重复
应用像通常一样处理单个语句结果。
当管道中所有查询的结果都返回时,PQgetResult
返回一个结果,其包含状态值PGRES_PIPELINE_SYNC
。
客户端可以选择延迟结果处理,直到完整的管道被发送,或者与管道中发送的更多的查询交错在一起;参见Section 32.5.1.4。
PQgetResult
的行为与普通异步处理相同,除了它可能包含新的
PGresult
类型 PGRES_PIPELINE_SYNC
和 PGRES_PIPELINE_ABORTED
。
PGRES_PIPELINE_SYNC
对应于管道中每个
PQpipelineSync
或
PQsendPipelineSync
的相应点,准确报告一次。
PGRES_PIPELINE_ABORTED
在首次错误及所有后续结果中替代正常查询结果,
直到下一个 PGRES_PIPELINE_SYNC
;
参见 Section 32.5.1.3。
PQisBusy
、PQconsumeInput
、等正常操作,在处理管道结果时。
特别是,在管道中间调用PQisBusy
时,如果到目前为止发出的所有查询的结果都已被消耗,则返回0。
libpq不向应用提供有关当前正在处理的查询的任何信息(除了PQgetResult
返回空,以表示我们开始返回下一个查询的结果)。
应用必须保持跟踪它发送查询的顺序,以将它们与相应的结果相关联。
应用通常会为此使用状态机或FIFO队列。
从客户端的角度来看,在PQresultStatus
返回
PGRES_FATAL_ERROR
之后,管道被标记为中止。
PQresultStatus
将为中止管道中剩余的每个排队操作报告
PGRES_PIPELINE_ABORTED
结果。对于
PQpipelineSync
或
PQsendPipelineSync
,结果报告为
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并刷新发送缓冲区的方式来标记管道中的同步点。 它用作隐式事务的定界符和错误恢复点;参见Section 32.5.1.3。
int PQpipelineSync(PGconn *conn);
成功就返回1。 如果连接不是管道模式或者发送sync message失败,则返回0。
PQsendPipelineSync
#通过发送一个 同步消息 来标记流水线中的同步点,而不刷新发送缓冲区。这作为隐式事务的分隔符和错误恢复点; 参见 Section 32.5.1.3。
int PQsendPipelineSync(PGconn *conn);
返回1表示成功。如果连接不处于流水线模式或发送
同步消息
失败,则返回0。
注意,消息本身不会自动刷新到服务器;如有必要,请使用
PQflush
。
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;
当单个管道包含多个事务时,管道的作用更小,而且更复杂(参见Section 32.5.1.3)。
[15] 客户端将阻止尝试发送查询到服务器,但服务器将阻止从它已经处理的查询向客户端发送结果。 这只有当客户端在它切换到处理从服务器的输入之前发送足够的查询来填充其输出缓冲区和服务器的接收缓冲区,才会发生这种情况,但很难准确预测何时将发生。