29.8. 冲突 #
逻辑复制的行为在一点上类似于普通 DML 操作:即使订阅节点上的数据已在本地被修改,传入的变更仍会更新它。如果传入数据违反了任何约束,复制就会停止,这种情况称为冲突。在复制 UPDATE 或
DELETE 操作时,找不到目标数据也被视为冲突,但不会报错,这类操作会被直接跳过。
在以下冲突场景中,会触发额外日志,并收集冲突统计信息(显示在
pg_stat_subscription_stats
视图中):
insert_exists#插入某行时违反了一个
NOT DEFERRABLE唯一约束。请注意,若要在日志中记录冲突键的来源和提交时间戳详情,订阅端应启用track_commit_timestamp。这种情况下,系统会持续报错,直到手工解决该冲突。update_origin_differs#更新一行,而该行此前已被另一来源修改。请注意,只有在订阅端启用了
track_commit_timestamp时才能检测到这种冲突。目前,无论本地行的来源是什么,更新都会照常应用。update_exists#某行更新后的值违反了一个
NOT DEFERRABLE唯一约束。请注意,若要在日志中记录冲突键的来源和提交时间戳详情,订阅端应启用track_commit_timestamp。这种情况下,系统会持续报错,直到手工解决该冲突。另请注意,在更新分区表时,如果更新后的行值满足了另一个分区的约束,导致该行被插入到新分区中,而新行又违反了一个NOT DEFERRABLE唯一约束,则可能出现insert_exists冲突。update_deleted#待更新的元组已被其他复制源并发删除。在这种情况下,更新会被直接跳过。请注意,只有在订阅端同时启用了
track_commit_timestamp和retain_dead_tuples时,才能检测到这种冲突。另请注意,如果元组因表被截断而找不到,则只会产生update_missing冲突;如果元组是被同一复制源删除的,也会只产生update_missing冲突。update_missing#要更新的行未找到。在这种情况下,该更新会被直接跳过。
delete_origin_differs#删除一行,而该行此前已被另一来源修改。请注意,只有在订阅端启用了
track_commit_timestamp时才能检测到这种冲突。目前,无论本地行的来源是什么,删除都会照常应用。delete_missing#要删除的行未找到。在这种情况下,该删除会被直接跳过。
multiple_unique_conflicts#插入或更新某行时违反了多个
NOT DEFERRABLE唯一约束。请注意,若要在日志中记录冲突键的来源和提交时间戳详情,请确保订阅端启用了track_commit_timestamp。这种情况下,系统会持续报错,直到手工解决该冲突。
还存在其他冲突场景,例如排他约束违规。目前,我们不会在日志中为它们提供额外细节。
逻辑复制冲突的日志格式如下:
LOG: conflict detected on relation "schemaname.tablename": conflict=conflict_typeDETAIL:detailed_explanation[:detail_values[, ... ]]. 其中detail_values可以是以下之一:key(column_name[, ...])=(column_value[, ...])local row[(column_name[, ...])=](column_value[, ...])remote row[(column_name[, ...])=](column_value[, ...])replica identity{(column_name[, ...])=(column_value[, ...]) | full [(column_name[, ...])=](column_value[, ...])}
日志会提供以下信息:
LOGschemaname.tablename标识发生冲突的本地关系。conflict_type是发生的冲突类型(例如insert_exists、update_exists)。
DETAILdetailed_explanation包括修改本地行的事务的来源、事务 ID 以及提交时间戳(如果可用)。key部分包含本地行的键值;该本地行在insert_exists、update_exists或multiple_unique_conflicts冲突中违反了唯一约束。local row部分包含本地行;当update_origin_differs或delete_origin_differs冲突中,本地行的来源与远端行不同,或者在insert_exists、update_exists或multiple_unique_conflicts冲突中,该键值与远端行冲突时,会记录这部分内容。remote row部分包含导致冲突的远端插入或更新操作中的新行。注意,对于更新操作,如果某个列值未变化且已经过 TOAST 处理,则新行中的该列值会为 null。replica identity部分包含用于查找要更新或删除的现有本地行的复制标识键值。如果本地关系被标记为REPLICA IDENTITY FULL,这里也可能包含整行值。column_name是列名。对于local row、remote row以及replica identity full这几种情况,只有当用户没有权限访问该表的全部列时,才会记录列名。如果记录了列名,它们的顺序与对应列值的顺序一致。column_value是列值。较大的列值会被截断到 64 字节。请注意,在
multiple_unique_conflicts冲突场景中,会生成多条detailed_explanation和detail_values行,每条都描述一个不同唯一约束对应的冲突信息。
逻辑复制操作会以订阅所有者角色的权限执行。目标表上的权限失败会导致复制冲突;目标表上启用的、且订阅所有者受其约束的行级安全也会导致复制冲突,而不考虑任何策略通常是否会拒绝当前复制的 INSERT、UPDATE、DELETE 或
TRUNCATE。未来版本的 PostgreSQL
可能会取消这一行级安全限制。
会产生错误的冲突会使复制停止;这类冲突必须由用户手工解决。有关冲突的详细信息可以在订阅端的服务器日志中找到。
解决方法可以是修改订阅端上的数据或权限,使其不再与传入变更冲突;也可以跳过与现有数据冲突的事务。当冲突导致错误时,复制不会继续,逻辑复制工作进程会向订阅端的服务器日志输出如下消息:
ERROR: conflict detected on relation "public.test": conflict=insert_exists DETAIL: Could not apply remote change: remote row (1, 'remote'). Key already exists in unique index "test_pkey", modified locally in transaction 800 at 2026-01-16 18:15:25.652759+09: key (c)=(1), local row (1, 'local'). CONTEXT: processing remote data for replication origin "pg_16395" during "INSERT" for replication target relation "public.test" in transaction 725 finished at 0/014C0378
可以从服务器日志中找到包含违规变更的事务的 LSN 以及复制源名称(在上述示例中分别是 LSN 0/014C0378 和复制源 pg_16395)。可以使用
ALTER SUBSCRIPTION ... SKIP
并提供完成 LSN(即 LSN 0/014C0378)来跳过产生冲突的事务。这个完成 LSN
可以是该事务在发布端提交或预备时的 LSN。或者,也可以调用
pg_replication_origin_advance() 函数来跳过该事务。在使用此函数之前,需要暂时禁用订阅,可以使用
ALTER SUBSCRIPTION ... DISABLE,或者让订阅使用
disable_on_error
选项。然后,可以使用 pg_replication_origin_advance() 函数,传入 node_name(即 pg_16395)以及完成 LSN 的下一个 LSN(即 0/014C0379)。当前各复制源的位置可以在
pg_replication_origin_status 系统视图中查看。请注意,跳过整个事务也会跳过那些本来可能不违反任何约束的变更,这很容易导致订阅端不一致。关于冲突行的其他细节,例如其来源和提交时间戳,也可以在日志的
DETAIL 行中看到。但请注意,这些信息只有在订阅端启用了
track_commit_timestamp
时才可用。用户可以利用这些信息来决定是保留本地更改,还是采用远端变更。例如,上述日志中的 DETAIL 行表明现有行是在本地被修改的。用户可以手工让远端变更胜出。
当
streaming
模式为 parallel 时,失败事务的完成 LSN 可能不会被记录。这种情况下,可能需要把 streaming 模式改为 on 或
off,并再次触发相同的冲突,以便将失败事务的完成 LSN
写入服务器日志。关于完成 LSN 的用法,请参见
ALTER SUBSCRIPTION ... SKIP。