阅读(3595) (11)

PostgreSQL 外部数据包装器中的行锁定

2021-08-24 10:47:22 更新

如果一个 FDW 的底层存储机制具有锁定行的概念来阻止对行的并发更新,通常值得 FDW 去执行行级锁定以尽可能接近在普通PostgreSQL表中所实际使用的语义。涉及这个问题有多种考虑。

要做出的一个关键决定是执行早期锁定还是晚期锁定。在早期锁定中,当一行被第一次从底层存储中检索到时,它会被锁定;而在晚期锁定中,只有当行需要被锁定时才锁定它(由于某些行可能被本地检查的限制或者连接条件抛弃,所以会出现不同)。早期锁定更加简单并且能避免额外地与远程存储交互,但是可能会导致一些不需要锁定的行也被锁定,最终造成并发性下降甚至意外的死锁。还有,只有在要被锁定的行可以在后期唯一地重新标识时才可以用晚期锁定。较好的行标识符应该能标识行的特定版本,就像 PostgreSQLTID 那样。

默认情况下,PostgreSQL在与 FDW 交互时会忽略锁定考虑,但是 FDW 可以在没有核心代码显式支持的情况下执行早期锁定。第 56.2.5 节中描述的 API 函数(在PostgreSQL 9.5 中加入)允许 FDW 按照意愿使用晚期锁定。

一个额外的考虑是在READ COMMITTED隔离模式中,PostgreSQL可能需要对某个目标元组的更新版本进行限制以及连接条件的重新检查。重新检查连接条件要求重新获得之前连接成目标元组的非目标行拷贝。在标准PostgreSQL表的情况下,这可以通过在连接投影出的列列表中包括非目标表的 TID 并且在需要时重新取得非目标行来做到。这种方法可以让连接数据集保持紧凑,但是它要求代价较低的重新取得元组的功能,还有 TID 要能够唯一地标识要被重新取得的行版本。因此,默认情况下用于外部表的方法是将整个外部表元组的拷贝包括在从连接投影出的列列表中。这不会对 FDW 有特殊的要求,但是会导致归并和哈希连接性能下降。要满足重新取得元组需求的 FDW 可以选择第一种方式。

对于在外部表上的UPDATE或者DELETE,推荐目标表上的ForeignScan操作在它取得的行上执行早期锁定(可能通过SELECT FOR UPDATE的等效体)。通过在规划时比较一个表的 relid 和root->parse->resultRelation或在执行时使用 ExecRelationIsTargetRelation(),一个 FDW 可以检测该表是否为UPDATE/DELETE的目标。另一种可能性是在ExecForeignUpdate或者ExecForeignDelete回调中执行晚期锁定,但是对此没有特别的支持。

对于通过SELECT FOR UPDATE/SHARE命令指定要被锁定的外部表,ForeignScan操作同样可以通过用SELECT FOR UPDATE/SHARE的等效体取元组来执行早期锁定。要执行晚期锁定,请提供第 56.2.5 节中定义的回调函数。在GetForeignRowMarkType中,根据请求的锁长度来选择行标记选项ROW_MARK_EXCLUSIVEROW_MARK_NOKEYEXCLUSIVEROW_MARK_SHARE或者 ROW_MARK_KEYSHARE(不管选择哪一种选项,核心代码都会做同样的事情)。在别的地方,可以在规划时用get_plan_rowmark或者在执行时用ExecFindRowMark来检测一个外部表是否被指定由这种类型的命令锁定。你必须不仅仅检测是否返回了一个非空的行标记结构,还要检测它的strength域不是 LCS_NONE

最后,对于在UPDATEDELETE或者SELECT FOR UPDATE/SHARE命令中使用但是没有被指定要行锁定的外部表,你可以在看到锁长度LCS_NONE时通过使用GetForeignRowMarkType选择选项 ROW_MARK_REFERENCE来把默认选择覆盖为拷贝整个行。 这将导致用那个值作为markType来调用RefetchForeignRow。它应该接着重新取得该行而不获取任何新锁(如果你有一个GetForeignRowMarkType函数,但是不想重新取未锁定的行,可为LCS_NONE选择选项 ROW_MARK_COPY)。

更多信息可见src/include/nodes/lockoptions.h,以及src/include/nodes/plannodes.hRowMarkTypePlanRowMark的注释,还有src/include/nodes/execnodes.hExecRowMark的注释。