

# 了解双活冲突
<a name="Appendix.PostgreSQL.CommonDBATasks.pgactive.actact.replication"></a>

如果您在双活模式下使用 pgactive，在从多个节点写入同一个表时可能会造成数据冲突。一些集群化系统会使用分布式锁来防止并发访问，不过 pgactive 采用的是乐观方法，更适合分布在不同地理位置的应用程序。

一些数据库集群系统使用分布式锁来防止并发数据访问。但是，此方法只适合距离非常近的服务器，不支持分布在不同地理位置的应用程序，因为此方法需要极低的延迟才能实现良好的性能。pgactive 扩展没有使用分布式锁（这是一种悲观方法），而是使用乐观方法。这意味着：
+ 可帮助您尽可能避免冲突。
+ 允许出现某些类型的冲突。
+ 发生冲突时会提供冲突解决方案。

通过这种方法，您在构建分布式应用程序时具有更好的灵活性。

## 发生冲突的原因
<a name="Appendix.PostgreSQL.CommonDBATasks.pgactive.actact.howconflicts"></a>

节点间冲突是由事件序列引起的，如果涉及的所有事务同时发生在同一个节点上，则不会出现节点间冲突。由于节点仅在事务提交后交换更改，因此每个事务在其提交的节点上都是分别有效的，但如果事务在另一个同时完成其他工作的节点上运行，就会无效。由于 pgactive 的应用操作本质上是在其他节点上重播事务，因此，如果正在应用的事务与在接收节点上提交的事务之间存在冲突，则重播操作会失败。

 当所有事务都运行在单个节点上时，大多数冲突不会发生的原因是 PostgreSQL 采用了事务间通信机制来防止冲突，包括：
+ UNIQUE 索引
+ SEQUENCE
+ 行和关系锁定
+ SERIALIZABLE 依赖关系跟踪

所有这些机制都是在事务之间进行通信的方法，用于防止出现意外并发问题

pgactive 不使用分布式事务管理器或锁管理器，因此可以实现低延迟并很好地处理网络分区。但是，这意味着不同节点上事务的运行是彼此完全隔离的。虽然隔离通常可以提高数据库的一致性，但在这种情况下，您需要减少隔离以防止冲突。

## 冲突类型
<a name="Appendix.PostgreSQL.CommonDBATasks.pgactive.actact.conflicttypes"></a>

可能发生的冲突包括：

**Topics**
+ [PRIMARY KEY 或 UNIQUE 冲突](#Appendix.PostgreSQL.CommonDBATasks.pgactive.actact.conflict1)
+ [INSERT/INSERT 冲突](#Appendix.PostgreSQL.CommonDBATasks.pgactive.actact.conflict2)
+ [违反多个 UNIQUE 约束的 INSERT](#Appendix.PostgreSQL.CommonDBATasks.pgactive.actact.conflict3)
+ [UPDATE/UPDATE 冲突](#Appendix.PostgreSQL.CommonDBATasks.pgactive.actact.conflict4)
+ [PRIMARY KEY 上的 UPDATE 冲突](#Appendix.PostgreSQL.CommonDBATasks.pgactive.actact.conflict5)
+ [违反多个 UNIQUE 约束的 UPDATE](#Appendix.PostgreSQL.CommonDBATasks.pgactive.actact.conflict6)
+ [UPDATE/DELETE 冲突](#Appendix.PostgreSQL.CommonDBATasks.pgactive.actact.conflict7)
+ [INSERT/UPDATE 冲突](#Appendix.PostgreSQL.CommonDBATasks.pgactive.actact.conflict8)
+ [DELETE/DELETE 冲突](#Appendix.PostgreSQL.CommonDBATasks.pgactive.actact.conflict9)
+ [外键约束冲突](#Appendix.PostgreSQL.CommonDBATasks.pgactive.actact.conflict10)
+ [排除约束冲突](#Appendix.PostgreSQL.CommonDBATasks.pgactive.actact.conflict11)
+ [全局数据冲突](#Appendix.PostgreSQL.CommonDBATasks.pgactive.actact.conflict12)
+ [锁冲突和死锁中止](#Appendix.PostgreSQL.CommonDBATasks.pgactive.actact.conflict13)
+ [分歧冲突](#Appendix.PostgreSQL.CommonDBATasks.pgactive.actact.conflict14)

### PRIMARY KEY 或 UNIQUE 冲突
<a name="Appendix.PostgreSQL.CommonDBATasks.pgactive.actact.conflict1"></a>

当多个操作试图以单个节点上不可能出现的方式修改同一个行键时，就会发生行冲突。这些冲突体现了最常见的数据冲突类型。

pgactive 采用上次更新获胜处理方法或者您的自定义冲突处理程序，来解决检测到的冲突。

行冲突包括：
+ INSERT 与 INSERT
+ INSERT 与 UPDATE
+ UPDATE 与 DELETE
+ INSERT 与 DELETE
+ DELETE 与 DELETE
+ INSERT 与 DELETE

### INSERT/INSERT 冲突
<a name="Appendix.PostgreSQL.CommonDBATasks.pgactive.actact.conflict2"></a>

这是最常见的冲突，当两个不同节点上的 INSERT 操作创建具有相同 PRIMARY KEY 值的元组（或在没有 PRIMARY KEY 时，使用相同的 UNIQUE 约束值），就会发生这种冲突。

pgactivelink 使用来自原始主机的时间戳来保留最新的元组，以此解决 INSERT 冲突。您可以使用自定义冲突处理程序覆盖此默认行为。虽然此过程不需要管理员专门采取操作，但请注意，pgactivelink 会丢弃所有节点上的其中一个 INSERT 操作。除非您的自定义处理程序实施自动数据合并，否则不会自动进行数据合并。

pgactivelink 只能解决涉及单个约束违规的冲突。如果某个 INSERT 违反了多个 UNIQUE 约束，您必须实施其他冲突解决策略。

### 违反多个 UNIQUE 约束的 INSERT
<a name="Appendix.PostgreSQL.CommonDBATasks.pgactive.actact.conflict3"></a>

INSERT/INSERT 冲突可能违反多个 UNIQUE 约束，包括 PRIMARY KEY。pgactivelink 只能处理涉及单个 UNIQUE 约束的冲突。当冲突违反多个 UNIQUE 约束时，应用工作线程会失败并返回以下错误：

`multiple unique constraints violated by remotely INSERTed tuple.`

在较早的版本中，这种情况生成的是“分歧唯一性冲突”错误。

要解决这些冲突，您必须手动采取操作。您可以对发生冲突的本地元组执行 DELETE，或者对元组执行 UPDATE 以使用新的远程元组来消除冲突。请注意，您可能需要解决多个相互冲突的元组。目前，pgactivelink 没有提供内置功能用于忽略、丢弃或合并违反多个唯一约束的元组。

**注意**  
有关更多信息，请参阅“违反多个 UNIQUE 约束的 UPDATE”。

### UPDATE/UPDATE 冲突
<a name="Appendix.PostgreSQL.CommonDBATasks.pgactive.actact.conflict4"></a>

当两个节点并发修改同一个元组而没有更改其 PRIMARY KEY 时，就会发生此冲突。pgactivelink 使用上次更新获胜逻辑或您的自定义冲突处理程序（如果已定义）来解决这些冲突。PRIMARY KEY 对于元组匹配和冲突解决至关重要。对于没有 PRIMARY KEY 的表，pgactivelink 拒绝 UPDATE 操作，并显示以下错误：

`Cannot run UPDATE or DELETE on table (tablename) because it does not have a primary key.`

### PRIMARY KEY 上的 UPDATE 冲突
<a name="Appendix.PostgreSQL.CommonDBATasks.pgactive.actact.conflict5"></a>

pgactive 在处理 PRIMARY KEY 更新时存在限制。虽然您可以对 PRIMARY KEY 执行 UPDATE 操作，但对于这些操作，pgactive 无法使用最后更新获胜逻辑自动解决冲突。您必须确保 PRIMARY KEY 更新不会与现有值冲突。如果在 PRIMARY KEY 更新期间发生冲突，这些冲突就会成为分歧冲突，需要您手动干预。有关处理这些情况的更多信息，请参阅[分歧冲突](#Appendix.PostgreSQL.CommonDBATasks.pgactive.actact.conflict14)。

### 违反多个 UNIQUE 约束的 UPDATE
<a name="Appendix.PostgreSQL.CommonDBATasks.pgactive.actact.conflict6"></a>

当传入的 UPDATE 违反多个 UNIQUE 约束或 PRIMARY KEY 值时，pgactivelink 无法应用上次更新获胜冲突解决方案。此行为类似于出现多个约束冲突的 INSERT 操作。这些情况会造成分歧冲突，需要您手动干预。有关更多信息，请参阅[分歧冲突](#Appendix.PostgreSQL.CommonDBATasks.pgactive.actact.conflict14)。

### UPDATE/DELETE 冲突
<a name="Appendix.PostgreSQL.CommonDBATasks.pgactive.actact.conflict7"></a>

当一个节点对一行执行 UPDATE 而另一个节点同时对该行执行 DELETE 时，就会出现此冲突。在这种情况下，重播时会发生 UPDATE/DELETE 冲突。除非您的自定义冲突处理程序另行规定，否则解决方案是丢弃在 DELETE 之后到达的所有 UPDATE。

pgactivelink 需要一个 PRIMARY KEY 来匹配元组并解决冲突。对于没有 PRIMARY KEY 的表，表会拒绝 DELETE 操作，并显示以下错误：

`Cannot run UPDATE or DELETE on table (tablename) because it does not have a primary key.`

**注意**  
pgactivelink 无法区分 UPDATE/DELETE 和 INSERT/UPDATE 冲突。在这两种情况下，UPDATE 均会影响不存在的行。由于异步复制以及节点之间缺乏重播排序，pgactivelink 无法确定是在对新行（尚未收到 INSERT）还是对已删除行执行 UPDATE 操作。在这两种情况下，pgactivelink 都会丢弃 UPDATE。

### INSERT/UPDATE 冲突
<a name="Appendix.PostgreSQL.CommonDBATasks.pgactive.actact.conflict8"></a>

这种冲突可能发生在多节点环境中。当一个节点 INSERT 一行数据，第二个节点对该行执行 UPDATE，而第三个节点在收到原始 INSERT 之前就接收到 UPDATE 时，就会发生这种情况。默认情况下，除非您的自定义冲突触发器另行规定，否则 pgactivelink 会通过放弃 UPDATE 来解决这些冲突。请注意，这种解决方法可能会导致节点间的数据不一致。有关类似场景及其处理方法的更多信息，请参阅 [UPDATE/DELETE 冲突](#Appendix.PostgreSQL.CommonDBATasks.pgactive.actact.conflict7)。

### DELETE/DELETE 冲突
<a name="Appendix.PostgreSQL.CommonDBATasks.pgactive.actact.conflict9"></a>

当两个不同的节点并发删除同一个元组时，就会发生这种冲突。pgactivelink 认为这些冲突无害，因为两个 DELETE 操作会产生相同的最终结果。在此场景中，pgactivelink 可以安全地忽略其中一个 DELETE 操作，而不会影响数据一致性。

### 外键约束冲突
<a name="Appendix.PostgreSQL.CommonDBATasks.pgactive.actact.conflict10"></a>

对现有本地数据应用远程事务时，FOREIGN KEY 约束可能会导致冲突。发生这些冲突的情况通常是应用事务的顺序，不同于事务在发起节点上的逻辑顺序。

默认情况下，pgactive 会将 session\$1replication\$1role 作为 `replica` 来应用更改，这会在复制过程中绕过外键检查。在双活配置中，这可能会导致外键违规。大多数违规行为都是暂时性的，当复制进度跟上时就会解决。但是，由于 pgactive 不支持跨节点行锁定，因此可能会出现悬挂外键。

这种行为是具备分区容错性的异步双活系统所固有的。例如，节点 A 可能会插入新的子行，而节点 B 同时在删除其父行。系统无法阻止节点之间这种类型的并发修改。

为尽可能减少外键冲突，我们给出了以下建议：
+ 将外键关系限制为密切相关的实体。
+ 尽可能从单个节点修改相关实体。
+ 选择极少需要进行修改的实体。
+ 针对修改实施应用程序级的并发控制。

### 排除约束冲突
<a name="Appendix.PostgreSQL.CommonDBATasks.pgactive.actact.conflict11"></a>

 pgactivelink 不支持排除约束，并会限制创建这种约束。

**注意**  
在将现有的独立数据库转换为 pgactivelink 数据库时，请手动删除所有排除约束。

在分布式异步系统中，不可能保证所有行集都没有违反约束。这是因为不同节点上的所有事务都是完全隔离的。排除约束可能导致重播死锁，在这种情况下，由于排除约束违规，重播无法从任何节点进展到另一个节点。

如果您强制 pgactivelink 创建排除约束，或者在将独立数据库转换到 pgactivelink 时不删除现有排除约束，则复制很可能会中断。要恢复复制进度，请删除或更改与传入的远程元组冲突的本地元组，这样便可以应用远程事务。

### 全局数据冲突
<a name="Appendix.PostgreSQL.CommonDBATasks.pgactive.actact.conflict12"></a>

使用 pgactivelink 时，当节点具有不同的全局 PostgreSQL 系统级数据（例如角色）时，就可能会发生冲突。这些冲突的结果是操作（主要是 DDL）会成功并在一个节点上提交，但无法应用于其他节点。

如果一个节点上有某个用户但另一个节点上没有，就会出现复制问题：
+ Node1 有名为 `fred` 的用户，但是 Node2 上没有该用户
+ 当 `fred` 在 Node1 上创建表时，表在复制时的所有者是 `fred`
+ 将此 DDL 命令应用于 Node2 时，命令会失败，因为用户 `fred` 不存在
+ 在失败后，会在 Node2 上的 PostgreSQL 日志中生成 ERROR 条目并递增 `pgactive.pgactive_stats.nr_rollbacks` 计数器

**解决方案：**在 Node2 上创建用户 `fred`。该用户不必具备相同的权限，但必须存在于两个节点上。

如果一个节点上有某个表但另一个节点上没有，则数据修改操作会失败：
+ Node1 上有名为 `foo` 的表，但 Node2 上没有
+ Node1 上对 `foo` 表执行的任何 DML 操作，在复制到 Node2 时会失败

**解决方案：**在 Node2 上创建具有相同结构的表 `foo`。

**注意**  
pgactivelink 目前不复制 CREATE USER 命令或 DDL 操作。DDL 复制功能计划在未来版本中发布。

### 锁冲突和死锁中止
<a name="Appendix.PostgreSQL.CommonDBATasks.pgactive.actact.conflict13"></a>

由于 pgactive 应用进程的运行方式类似于普通用户会话，因此这些进程遵循标准的行和表锁定规则。这可能会导致 pgactivelink 应用进程需要等待用户事务或其他应用进程持有的锁被释放。

以下类型的锁会影响应用进程：
+ 用户会话施加的显式表级锁（LOCK TABLE...）
+ 用户会话施加的显式行级锁（SELECT ... FOR UPDATE/FOR SHARE）
+ 来自外键的锁定
+ 由于行 UPDATE、INSERT 或 DELETE 而导致的隐式锁定，这可能源自本地活动，也可能是从其他服务器应用的

在下列操作之间可能会出现死锁：
+ pgactivelink 应用进程和用户事务
+ 两个应用进程

发生死锁时，PostgreSQL 的死锁检测器会终止其中一个出现问题的事务。如果 pgactivelink 应用工作线程的进程终止，则会自动重试而且通常会成功。

**注意**  
这些问题是暂时性的，通常无需管理员干预。如果由于空闲用户会话锁定导致某个应用进程被长时间阻止，您可以终止该用户会话来恢复复制。这种情况类似于某个用户持有的长时间锁定影响到了其他用户会话。
要识别与锁定相关的重播延迟，请在 PostgreSQL 中启用 `log_lock_waits` 功能。

### 分歧冲突
<a name="Appendix.PostgreSQL.CommonDBATasks.pgactive.actact.conflict14"></a>

当各节点之间本应相同的数据意外地变得不同时，就会发生分歧冲突。虽然这些冲突不应该发生，但在当前的实施中，无法可靠地预防所有冲突。

**注意**  
 在一行上，如果有某个节点在所有节点处理更改之前更改该行的主键，则修改行的 PRIMARY KEY 会导致分歧冲突。请避免更改主键，或将更改限制在某个指定节点上。有关更多信息，请参阅 [PRIMARY KEY 上的 UPDATE 冲突](#Appendix.PostgreSQL.CommonDBATasks.pgactive.actact.conflict5)。

涉及行数据的分歧冲突通常需要管理员干预。要解决这些冲突，您必须手动调整一个节点上的数据来与其他节点相匹配，同时使用 `pgactive.pgactive_do_not_replicate` 暂时禁用复制。如果您按照文档说明使用 pgactive 并避免使用标记为不安全的设置或函数，则不应出现这些冲突。

 作为管理员，您必须手动解决这些冲突。根据冲突类型，您需要使用诸如 `pgactive.pgactive_do_not_replicate` 之类的高级选项。请谨慎使用这些选项，因为使用不当会导致情况恶化。由于可能出现的冲突各种各样，我们无法提供通用的解决方案说明。

当不同节点之间本应相同的数据意外地变得不同时，就会发生分歧冲突。虽然这些冲突不应该发生，但在当前的实施中，无法可靠地预防所有此类冲突。

## 避免或容忍冲突
<a name="Appendix.PostgreSQL.CommonDBATasks.pgactive.actact.avoidconflicts"></a>

 在大多数情况下，您可以使用合适的应用程序设计，从而避免冲突或使应用程序具备容忍冲突的能力。

 只有在多个节点上同时进行多个操作时才会发生冲突。为避免冲突，请采取以下方法：
+ 仅向一个节点写入
+ 写入各个节点上的独立数据库子集（例如，向每个节点分配一个单独的架构）

对于 INSERT 与 INSERT 冲突，请使用全局序列来彻底防止冲突。

 如果您的应用场景不能接受冲突，请考虑在应用程序级别实施分布式锁定。通常，最好的方法是对应用程序进行设计，使其与 pgactive 的冲突解决机制配合使用，而不是尝试预防所有冲突。有关更多信息，请参阅 [冲突类型](#Appendix.PostgreSQL.CommonDBATasks.pgactive.actact.conflicttypes)。

## 冲突日志记录
<a name="Appendix.PostgreSQL.CommonDBATasks.pgactive.actact.conflictlogging"></a>

pgactivelink 会将冲突事件记录到 `pgactive.pgactive_conflict_history` 表中，供您诊断和处理双活冲突。仅当 `pgactive.log_conflicts_to_table` 设置为 true 时，才会将冲突记录到此表。无论 `pgactive.log_conflicts_to_table` 的设置如何，当 log\$1min\$1messages 设置为 `LOG` 或 `lower` 时，pgactive 扩展还会将冲突记录到 PostgreSQL 日志文件中。

 使用冲突历史记录表可以：
+ 衡量您的应用程序造成冲突的频率
+ 确定在什么位置发生冲突
+ 改进应用程序以降低冲突率
+ 检测冲突解决方案无法达成所需结果的情况
+ 确定哪些地方需要用户定义的冲突触发器或更改应用程序设计

 对于行冲突，您可以选择记录行值。这由 `pgactive.log_conflicts_to_table` 设置控制。请注意：
+ 这是一个全局数据库级选项
+ 无法按各个表来控制对行值的记录
+ 字段数量、数组元素或字段长度没有限制
+ 如果您在处理数 MB 的行而这些行可能会触发冲突，则不建议启用此功能

 由于冲突历史记录表包含来自数据库中各个表的数据（每个表都可能采用不同架构），因此记录的行值存储为 JSON 字段。JSON 使用 `row_to_json` 创建，类似于直接从 SQL 中调用。PostgreSQL 不提供 `json_to_row` 函数，因此您需要特定于表的代码（采用 PL/pgSQL、PL/Python、PL/Perl 等）才能从记录的 JSON 中重新构造复合类型的元组。

**注意**  
对用户定义冲突的支持计划作为未来的扩展功能推出。