在使用涉及多个源的复制设置(包括循环复制)时,不同的源可能会尝试在副本上使用不同的数据更新同一行。NDB Cluster 复制中的冲突解决提供了一种通过允许使用用户定义的解决列来解决此类冲突的方法,该列用于确定是否应在副本上应用给定源上的更新。
NDB Cluster 支持的某些类型的冲突解决(NDB$OLD()
、NDB$MAX()
和 NDB$MAX_DELETE_WIN()
;NDB$MAX_INS()
和 NDB$MAX_DEL_WIN_INS()
)将此用户定义的列实现为 “时间戳” 列(尽管其类型不能为 TIMESTAMP
,如本节后面所述)。这些类型的冲突解决始终在逐行基础上应用,而不是在事务基础上应用。基于纪元的冲突解决函数 NDB$EPOCH()
和 NDB$EPOCH_TRANS()
比较纪元复制的顺序(因此这些函数是事务性的)。不同的方法可用于在副本上比较冲突发生时的解决列值,如本节后面所述;使用的方法可以设置为作用于单个表、数据库或服务器,或者作用于使用模式匹配的一组一个或多个表。有关在 mysql.ndb_replication
表的 db
、table_name
和 server_id
列中使用模式匹配的信息,请参阅 使用通配符匹配。
您还应该牢记,应用程序有责任确保解决列正确填充了相关值,以便解决函数在确定是否应用更新时可以做出适当的选择。
必须在源和副本上进行冲突解决的准备工作。以下列表中描述了这些任务
在写入二进制日志的源上,您必须确定发送哪些列(所有列或仅更新的列)。这是通过应用 mysqld 启动选项
--ndb-log-updated-only
(在本节后面描述)对整个 MySQL Server 进行的,或者通过在mysql.ndb_replication
表中放置适当的条目在单个或多个特定表上进行的(请参阅 ndb_replication 表)。注意如果您正在复制具有非常大的列的表(例如
TEXT
或BLOB
列),--ndb-log-updated-only
还可以用于减小二进制日志的大小,并避免由于超过max_allowed_packet
而可能发生的复制失败。有关此问题的更多信息,请参阅 第 19.5.1.20 节,“复制和 max_allowed_packet”。
在副本上,您必须确定要应用哪种类型的冲突解决(“最新时间戳获胜”、“相同时间戳获胜”、“主获胜”、“主获胜,完成事务” 或无)。这是使用
mysql.ndb_replication
系统表完成的,并应用于单个或多个特定表(请参阅 ndb_replication 表)。NDB Cluster 还支持读取冲突检测,即检测给定行在一个集群中的读取与同一行在另一个集群中的更新或删除之间的冲突。这需要通过将副本上的
ndb_log_exclusive_reads
设置为 1 来获取的独占读取锁。由冲突读取读取的所有行都记录在异常表中。有关更多信息,请参阅 读取冲突检测和解决。使用
NDB$MAX_INS()
或NDB$MAX_DEL_WIN_INS()
时,NDB
可以以幂等方式应用WRITE_ROW
事件,将此类事件映射到在传入行不存在时插入,或在传入行已存在时更新。在使用除
NDB$MAX_INS()
或NDB$MAX_DEL_WIN_INS()
之外的任何冲突解决函数时,如果行已存在,则始终拒绝传入写入。
在使用函数 NDB$OLD()
、NDB$MAX()
、NDB$MAX_DELETE_WIN()
、NDB$MAX_INS()
和 NDB$MAX_DEL_WIN_INS()
进行基于时间戳的冲突解决时,我们通常将用于确定更新的列称为 “时间戳” 列。但是,此列的数据类型永远不会是 TIMESTAMP
;相反,其数据类型应该是 INT
(INTEGER
) 或 BIGINT
。 “时间戳” 列还应该是 UNSIGNED
和 NOT NULL
。
本节后面讨论的 NDB$EPOCH()
和 NDB$EPOCH_TRANS()
函数通过比较在主 NDB Cluster 和辅助 NDB Cluster 上应用的复制纪元的相对顺序来工作,并且不使用时间戳。
我们可以在 “之前” 和 “之后” 图像方面查看更新操作,即应用更新之前和之后的表状态。通常,在使用主键更新表时,“之前” 图像并不十分重要;但是,当我们需要在每个更新的基础上确定是否在副本上使用更新的值时,我们需要确保将两个图像都写入源的二进制日志。这是通过 --ndb-log-update-as-write
选项为 mysqld 完成的,如本节后面所述。
冲突解决通常在可能发生冲突的服务器上启用。与日志记录方法选择类似,它是通过 mysql.ndb_replication
表中的条目启用的。
NBT_UPDATED_ONLY_MINIMAL
和 NBT_UPDATED_FULL_MINIMAL
可以与 NDB$EPOCH()
、NDB$EPOCH2()
和 NDB$EPOCH_TRANS()
一起使用,因为这些函数不需要 “之前” 值,这些值不是主键。需要旧值的冲突解决算法,例如 NDB$MAX()
和 NDB$OLD()
,不能与这些 binlog_type
值一起正常工作。
本节提供了有关可用于使用 NDB Replication 进行冲突检测和解决的函数的详细信息。
NDB$OLD()
如果 column_name
的值在源和副本上都相同,则应用更新;否则,不会在副本上应用更新,并且异常会写入日志。以下伪代码说明了这一点
if (source_old_column_value == replica_current_column_value)
apply_update();
else
log_exception();
此函数可用于 “相同值获胜” 冲突解决。这种类型的冲突解决确保不会从错误的源在副本上应用更新。
此函数使用源的 “之前” 图像中的列值。
NDB$MAX()
对于更新或删除操作,如果来自源的给定行的 “时间戳” 列值高于副本上的值,则应用该值;否则,不会在副本上应用该值。以下伪代码说明了这一点
if (source_new_column_value > replica_current_column_value)
apply_update();
此函数可用于 “最大时间戳获胜” 冲突解决。这种类型的冲突解决确保在发生冲突的情况下,最近更新的行的版本是持久存在的版本。
此函数对写入操作之间的冲突没有影响,除了使用与先前写入相同的 primary key 的写入操作始终被拒绝;仅当不存在使用相同 primary key 的任何写入操作时,它才会被接受和应用。您可以使用 NDB$MAX_INS()
来处理写入之间的冲突解决。
此函数使用源的 “之后” 图像中的列值。
NDB$MAX_DELETE_WIN()
这是对 NDB$MAX()
的一种变体。由于删除操作没有时间戳,使用 NDB$MAX()
的删除实际上被处理为 NDB$OLD
,但在某些用例中,这不是最佳选择。对于 NDB$MAX_DELETE_WIN()
,如果来自源的特定行添加或更新现有行的“时间戳”列值高于副本上的时间戳,则会应用该更新。但是,删除操作始终被视为具有更高的值。以下伪代码对此进行了说明
if ( (source_new_column_value > replica_current_column_value)
||
operation.type == "delete")
apply_update();
此函数可用于“最大时间戳,删除获胜”的冲突解决。这种类型的冲突解决确保在发生冲突时,删除或最近更新的行版本是持久存在的版本。
与 NDB$MAX()
一样,此函数使用来自源“之后”映像的列值。
NDB$MAX_INS()
此函数提供对冲突写操作的解决支持。此类冲突由“NDB$MAX_INS()”如下处理
如果没有冲突的写入,则应用此写入(这与
NDB$MAX()
相同)。否则,应用“最大时间戳获胜”冲突解决,如下所示
如果传入写入的时间戳大于冲突写入的时间戳,则应用传入操作。
如果传入写入的时间戳**不**大于,则拒绝传入写入操作。
在处理插入操作时,NDB$MAX_INS()
会比较来自源和副本的时间戳,如下面的伪代码所示
if (source_new_column_value > replica_current_column_value)
apply_insert();
else
log_exception();
对于更新操作,会将源的更新时间戳列值与副本的时间戳列值进行比较,如下所示
if (source_new_column_value > replica_current_column_value)
apply_update();
else
log_exception();
这与 NDB$MAX()
执行的操作相同。
对于删除操作,处理方式与 NDB$MAX()
(以及 NDB$OLD()
)执行的处理方式相同,如下所示
if (source_new_column_value == replica_current_column_value)
apply_delete();
else
log_exception();
NDB$MAX_DEL_WIN_INS()
此函数提供对冲突写操作的解决支持,以及类似于 NDB$MAX_DELETE_WIN()
的“删除获胜”解决。如下所示,NDB$MAX_DEL_WIN_INS()
处理写冲突
如果没有冲突的写入,则应用此写入(这与
NDB$MAX_DELETE_WIN()
相同)。否则,应用“最大时间戳获胜”冲突解决,如下所示
如果传入写入的时间戳大于冲突写入的时间戳,则应用传入操作。
如果传入写入的时间戳**不**大于,则拒绝传入写入操作。
如下所示,NDB$MAX_DEL_WIN_INS()
执行的插入操作处理可以用伪代码表示
if (source_new_column_value > replica_current_column_value)
apply_insert();
else
log_exception();
对于更新操作,会将源的更新时间戳列值与副本的时间戳列值进行比较,如下所示(再次使用伪代码)
if (source_new_column_value > replica_current_column_value)
apply_update();
else
log_exception();
删除使用“删除始终获胜”策略(与 NDB$MAX_DELETE_WIN()
相同)进行处理;始终应用 DELETE
,而不考虑任何时间戳值,如下面的伪代码所示
if (operation.type == "delete")
apply_delete();
对于更新和删除操作之间的冲突,此函数的行为与 NDB$MAX_DELETE_WIN()
相同。
NDB$EPOCH()
NDB$EPOCH()
函数跟踪在副本集群上应用的已复制纪元的顺序,相对于在副本上起源的更改。此相对排序用于确定源自副本的更改是否与任何本地起源的更改同时发生,因此可能存在冲突。
以下关于 NDB$EPOCH()
的描述大多数也适用于 NDB$EPOCH_TRANS()
。任何例外情况将在文本中注明。
NDB$EPOCH()
是非对称的,在一个双向复制配置(有时称为“主动-主动”复制)的 NDB 集群上运行。我们这里将它运行的集群称为主集群,另一个称为从集群。主集群上的副本负责检测和处理冲突,而从集群上的副本不参与任何冲突检测或处理。
当主集群上的副本检测到冲突时,它会在自己的二进制日志中注入事件以弥补这些冲突;这确保了从 NDB 集群最终与主集群对齐,从而防止主集群和从集群发生分歧。这种补偿和重新对齐机制要求主 NDB 集群始终赢得与从集群的任何冲突——也就是说,在发生冲突时,始终使用主集群的更改而不是从集群的更改。此“主集群始终获胜”规则具有以下含义
一旦在主集群上提交,更改数据的操作将完全持久化,不会因冲突检测和解决而撤消或回滚。
从主集群读取的数据完全一致。在主集群上提交的任何更改(本地或来自副本)都不会在以后被还原。
在从集群上更改数据的操作可能会在以后被还原,如果主集群确定它们存在冲突。
始终从从集群读取的单个行是自一致的,每行始终反映从集群提交的状态,或主集群提交的状态。
从从集群读取的行集不一定在给定时间点一致。对于
NDB$EPOCH_TRANS()
,这是一种瞬态状态;对于NDB$EPOCH()
,它可以是持久状态。假设一段足够长的时期内没有冲突,从 NDB 集群上的所有数据(最终)将与主集群上的数据保持一致。
NDB$EPOCH()
和 NDB$EPOCH_TRANS()
不需要任何用户模式修改,也不需要应用更改来提供冲突检测。但是,必须仔细考虑使用的模式以及使用的访问模式,以验证整个系统是否在规定的限制内运行。
每个 NDB$EPOCH()
和 NDB$EPOCH_TRANS()
函数都可以接受一个可选参数;这是用于表示纪元低 32 位的位数,应设置为不小于如下所示计算的值
CEIL( LOG2( TimeBetweenGlobalCheckpoints / TimeBetweenEpochs ), 1)
对于这些配置参数的默认值(分别为 2000 和 100 毫秒),这将给出 5 位的值,因此默认值 (6) 应该足够,除非使用其他值用于 TimeBetweenGlobalCheckpoints
、TimeBetweenEpochs
或两者。太小的值会导致误报,而太大的值会导致数据库中浪费过多的空间。
只要这些表已根据本节中其他地方描述的相同异常表模式规则(请参阅 NDB$OLD())定义,NDB$EPOCH()
和 NDB$EPOCH_TRANS()
都会将冲突行的条目插入到相关的异常表中。您必须在创建要使用它的数据表之前创建任何异常表。
与本节讨论的其他冲突检测函数一样,NDB$EPOCH()
和 NDB$EPOCH_TRANS()
是通过在 mysql.ndb_replication
表中包含相关条目来激活的(请参阅 ndb_replication 表)。在此场景中,主 NDB 集群和从 NDB 集群的角色完全由 mysql.ndb_replication
表条目确定。
由于 NDB$EPOCH()
和 NDB$EPOCH_TRANS()
使用的冲突检测算法是非对称的,因此您必须对主副本和从副本的 server_id
条目使用不同的值。
仅 DELETE
操作之间的冲突不足以使用 NDB$EPOCH()
或 NDB$EPOCH_TRANS()
触发冲突,并且在纪元中的相对位置无关紧要。
当前,在使用 NDB$EPOCH()
执行冲突检测时,适用以下限制
使用 NDB 集群纪元边界检测冲突,粒度与
TimeBetweenEpochs
(默认值:100 毫秒)成正比。最小冲突窗口是在两个集群上对相同数据同时进行更新时始终报告冲突的最小时间。这始终是一个非零长度的时间,并且大致与2 * (延迟 + 排队 + TimeBetweenEpochs)
成正比。这意味着——假设TimeBetweenEpochs
的默认值,忽略两个集群之间的任何延迟(以及任何排队延迟)——最小冲突窗口大小约为 200 毫秒。在查看预期的应用程序“竞争”模式时,应考虑此最小窗口。使用
NDB$EPOCH()
和NDB$EPOCH_TRANS()
函数的表需要额外的存储空间;每行需要 1 到 32 位的额外空间,具体取决于传递给函数的值。删除操作之间的冲突可能会导致主集群和从集群之间出现分歧。当两个集群同时删除一行时,可以检测到冲突,但不会记录冲突,因为该行已被删除。这意味着任何后续重新对齐操作的传播期间的进一步冲突无法检测到,这会导致分歧。
删除应在外部序列化,或仅路由到一个集群。或者,应以事务方式更新一个单独的行,其中包含此类删除和任何后续插入,以便可以跨行删除跟踪冲突。这可能需要在应用程序中进行更改。
当前,在使用
NDB$EPOCH()
或NDB$EPOCH_TRANS()
进行冲突检测时,仅支持双向“主动-主动”配置中的两个 NDB 集群。
NDB$EPOCH_TRANS()
NDB$EPOCH_TRANS()
扩展了 NDB$EPOCH()
函数。使用“主集群始终获胜”规则(请参阅 NDB$EPOCH())以相同的方式检测和处理冲突,但有一个额外的条件,即在发生冲突的同一事务中更新的任何其他行也被视为存在冲突。换句话说,NDB$EPOCH()
在从集群上重新对齐单个冲突行,而 NDB$EPOCH_TRANS()
重新对齐冲突事务。
此外,任何可检测地依赖于冲突事务的事务也被视为存在冲突,这些依赖关系由从集群二进制日志的内容确定。由于二进制日志仅包含数据修改操作(插入、更新和删除),因此仅使用重叠数据修改来确定事务之间的依赖关系。
NDB$EPOCH_TRANS()
与 NDB$EPOCH()
具有相同的条件和限制,此外还需要将所有事务 ID 记录在辅助节点的二进制日志中,使用 --ndb-log-transaction-id
设置为 ON
。这会增加可变数量的开销(每行最多 13 字节)。
参见 NDB$EPOCH()。
NDB$EPOCH2()
NDB$EPOCH2()
函数类似于 NDB$EPOCH()
,不同之处在于 NDB$EPOCH2()
支持使用双向复制拓扑的删除-删除处理。在这种情况下,主节点和辅助节点角色通过在每个源节点上将 ndb_conflict_role
系统变量设置为适当的值来分配(通常每个源节点分别设置为 PRIMARY
、SECONDARY
)。这样做后,辅助节点所做的修改将由主节点反映回辅助节点,然后辅助节点有条件地应用这些修改。
NDB$EPOCH2_TRANS()
NDB$EPOCH2_TRANS()
扩展了 NDB$EPOCH2()
函数。冲突的检测和处理方式相同,并为复制集群分配主节点和辅助节点角色,但需要额外满足以下条件:在发生冲突的同一事务中更新的任何其他行也被视为处于冲突状态。也就是说,NDB$EPOCH2()
会在辅助节点上重新调整各个冲突行,而 NDB$EPOCH_TRANS()
会重新调整冲突事务。
在 NDB$EPOCH()
和 NDB$EPOCH_TRANS()
使用每行、每次最后修改时间戳的元数据来确定主节点上来自辅助节点的传入复制行更改是否与本地提交的更改同时发生;同时发生的更改被视为冲突,随后会进行异常表更新和辅助节点重新调整。当主节点上删除一行后,将不再有任何最后修改时间戳可用以确定任何复制操作是否冲突时,就会出现问题,这意味着不会检测到冲突的删除操作。这会导致分歧,例如,在一个集群上执行的删除操作与另一个集群上同时执行的删除和插入操作冲突;这就是为什么在使用 NDB$EPOCH()
和 NDB$EPOCH_TRANS()
时,删除操作只能路由到一个集群。
NDB$EPOCH2()
通过以下方式绕过上述问题(在 PRIMARY 上存储有关已删除行的信息):忽略任何删除-删除冲突,并避免任何潜在的后续分歧。这是通过将成功应用于辅助节点并从辅助节点复制的任何操作反映回辅助节点来实现的。当操作返回到辅助节点时,它可以用来在辅助节点上重新应用由源自主节点的操作删除的操作。
在使用 NDB$EPOCH2()
时,您应该记住,辅助节点应用来自主节点的删除操作,删除新行,直到该行通过反映的操作恢复。从理论上讲,辅助节点上的后续插入或更新操作与来自主节点的删除操作冲突,但在这种情况下,我们选择忽略此冲突并允许辅助节点 “获胜”,以防止集群之间出现分歧。换句话说,在删除操作后,主节点不会检测到冲突,而是立即采用辅助节点的后续更改。因此,辅助节点的状态可能会在向最终(稳定)状态转换的过程中重新访问多个先前的已提交状态,其中一些状态可能是可见的。
您还应该注意,将所有操作从辅助节点反映回主节点会增加主节点日志二进制日志的大小,还会增加带宽、CPU 使用率和磁盘 I/O 的需求。
反映的操作在辅助节点上的应用取决于辅助节点上目标行的状态。可以通过检查 Ndb_conflict_reflected_op_prepare_count
和 Ndb_conflict_reflected_op_discard_count
状态变量来跟踪反映的更改是否已应用于辅助节点。应用的更改数量仅仅是这两个值之间的差值(注意 Ndb_conflict_reflected_op_prepare_count
始终大于或等于 Ndb_conflict_reflected_op_discard_count
)。
仅当以下两个条件都满足时,才会应用事件
行的存在(即行是否存在)与事件类型一致。对于删除和更新操作,该行必须已存在。对于插入操作,该行必须 不存在。
该行上次修改是由主节点完成的。可能是通过执行反映的操作完成的修改。
如果这两个条件都不满足,辅助节点会丢弃反映的操作。
要使用 NDB$OLD()
冲突解决函数,还需要为要使用此类型的冲突解决的每个 NDB
表创建异常表。在使用 NDB$EPOCH()
或 NDB$EPOCH_TRANS()
时,也是如此。该表的名称是应用冲突解决的表的名称,在名称后面追加 $EX
字符串。(例如,如果原始表的名称是 mytable
,则相应的异常表名称应该是 mytable$EX
。)创建异常表的语法如下所示
CREATE TABLE original_table$EX (
[NDB$]server_id INT UNSIGNED,
[NDB$]source_server_id INT UNSIGNED,
[NDB$]source_epoch BIGINT UNSIGNED,
[NDB$]count INT UNSIGNED,
[NDB$OP_TYPE ENUM('WRITE_ROW','UPDATE_ROW', 'DELETE_ROW',
'REFRESH_ROW', 'READ_ROW') NOT NULL,]
[NDB$CFT_CAUSE ENUM('ROW_DOES_NOT_EXIST', 'ROW_ALREADY_EXISTS',
'DATA_IN_CONFLICT', 'TRANS_IN_CONFLICT') NOT NULL,]
[NDB$ORIG_TRANSID BIGINT UNSIGNED NOT NULL,]
original_table_pk_columns,
[orig_table_column|orig_table_column$OLD|orig_table_column$NEW,]
[additional_columns,]
PRIMARY KEY([NDB$]server_id, [NDB$]source_server_id, [NDB$]source_epoch, [NDB$]count)
) ENGINE=NDB;
前四列是必需的。前四列的名称以及与原始表的主键列匹配的列不是关键的;但是,出于清晰度和一致性的考虑,我们建议您对 server_id
、source_server_id
、source_epoch
和 count
列使用此处显示的名称,并且对与原始表中的列匹配的列使用与原始表中相同的名称。
如果异常表使用一个或多个可选列 NDB$OP_TYPE
、NDB$CFT_CAUSE
或 NDB$ORIG_TRANSID
(在本节后面讨论),则每个必需列也必须使用前缀 NDB$
命名。如果需要,即使您未定义任何可选列,也可以使用 NDB$
前缀命名必需列,但在这种情况下,所有四个必需列都必须使用该前缀命名。
在这些列之后,构成原始表主键的列应按照它们用于定义原始表主键的顺序进行复制。复制原始主键列的列的数据类型应该与原始列的数据类型相同(或大于原始列的数据类型)。可以使用主键列的子集。
异常表必须使用 NDB
存储引擎。(本节后面显示了使用 NDB$OLD()
和异常表的示例。)
可以在复制的主键列之后可选地定义其他列,但不能在任何主键列之前;任何此类额外列不能是 NOT NULL
。NDB Cluster 支持三个额外的预定义可选列 NDB$OP_TYPE
、NDB$CFT_CAUSE
和 NDB$ORIG_TRANSID
,将在接下来的几段中介绍。
NDB$OP_TYPE
:可以使用此列获取导致冲突的操作类型。如果使用此列,请按照此处所示定义它
NDB$OP_TYPE ENUM('WRITE_ROW', 'UPDATE_ROW', 'DELETE_ROW',
'REFRESH_ROW', 'READ_ROW') NOT NULL
WRITE_ROW
、UPDATE_ROW
和 DELETE_ROW
操作类型表示用户启动的操作。REFRESH_ROW
操作是由冲突解决在补偿事务中生成的,这些补偿事务从检测到冲突的集群发送回原始集群。READ_ROW
操作是使用排他行锁定义的用户启动的读取跟踪操作。
NDB$CFT_CAUSE
:您可以定义一个可选列 NDB$CFT_CAUSE
,它提供已注册冲突的原因。如果使用此列,请按照此处所示定义它
NDB$CFT_CAUSE ENUM('ROW_DOES_NOT_EXIST', 'ROW_ALREADY_EXISTS',
'DATA_IN_CONFLICT', 'TRANS_IN_CONFLICT') NOT NULL
ROW_DOES_NOT_EXIST
可以作为 UPDATE_ROW
和 WRITE_ROW
操作的原因进行报告;ROW_ALREADY_EXISTS
可以作为 WRITE_ROW
事件的原因进行报告。DATA_IN_CONFLICT
在基于行的冲突函数检测到冲突时进行报告;TRANS_IN_CONFLICT
在基于事务的冲突函数拒绝属于完整事务的所有操作时进行报告。
NDB$ORIG_TRANSID
:如果使用 NDB$ORIG_TRANSID
列,则该列包含原始事务的 ID。此列应按如下所示定义
NDB$ORIG_TRANSID BIGINT UNSIGNED NOT NULL
NDB$ORIG_TRANSID
是由 NDB
生成的 64 位值。此值可用于关联属于来自相同或不同异常表的同一冲突事务的多个异常表条目。
不在原始表主键中的其他参考列可以命名为
或 colname
$OLD
。colname
$NEW
会引用更新和删除操作中的旧值(即包含 colname
$OLDDELETE_ROW
事件的操作)。
可用于引用插入和更新操作中的新值(换句话说,使用 colname
$NEWWRITE_ROW
事件、UPDATE_ROW
事件或这两种类型的事件的操作)。在冲突操作未为给定的参考列(不是主键)提供值的情况下,异常表行将包含 NULL
或该列的定义的默认值。
mysql.ndb_replication
表是在设置数据表进行复制时读取的,因此,在创建要复制的表之前,必须将对应于要复制的表的行插入到 mysql.ndb_replication
中。
可以使用多个状态变量来监控冲突检测。您可以通过 Ndb_conflict_fn_epoch
系统状态变量的当前值来查看自上次从当前状态重启此副本以来,由 NDB$EPOCH()
发现的冲突行数。
Ndb_conflict_fn_epoch_trans
提供直接由 NDB$EPOCH_TRANS()
发现的冲突行数。Ndb_conflict_fn_epoch2
和 Ndb_conflict_fn_epoch2_trans
分别显示由 NDB$EPOCH2()
和 NDB$EPOCH2_TRANS()
发现的冲突行数。实际重新调整的行数(包括由于其属于或依赖于与其他冲突行相同的事务而受到影响的行)由 Ndb_conflict_trans_row_reject_count
提供。
另一个服务器状态变量 Ndb_conflict_fn_max
提供了自上次启动 mysqld 以来,由于 “最大时间戳获胜” 冲突解决,导致行未应用到当前 SQL 节点的次数。 Ndb_conflict_fn_max_del_win
提供了基于 NDB$MAX_DELETE_WIN()
结果应用冲突解决的次数。
Ndb_conflict_fn_max_ins
跟踪使用 NDB$MAX_INS()
对写操作应用 “更大时间戳获胜” 处理的次数;状态变量 Ndb_conflict_fn_max_del_win_ins
提供了使用 NDB$MAX_DEL_WIN_INS()
实现的写操作 “相同时间戳获胜” 处理的次数。
自上次重新启动以来,由于在给定 mysqld 上 “相同时间戳获胜” 冲突解决导致行未应用的次数由全局状态变量 Ndb_conflict_fn_old
给出。除了增加 Ndb_conflict_fn_old
的值之外,未使用的行的主键还被插入到 异常表 中,如本节其他地方所述。
以下示例假设您已经有一个工作 NDB Cluster 复制设置,如 第 25.7.5 节,“准备 NDB Cluster 复制” 和 第 25.7.6 节,“启动 NDB Cluster 复制(单一复制通道)” 中所述。
NDB$MAX() 示例。 假设您希望在表 test.t1
上启用 “最大时间戳获胜” 冲突解决,使用列 mycol
作为 “时间戳”。这可以通过以下步骤完成
确保您已使用
--ndb-log-update-as-write=OFF
启动源 mysqld。在源上,执行以下
INSERT
语句INSERT INTO mysql.ndb_replication VALUES ('test', 't1', 0, NULL, 'NDB$MAX(mycol)');
注意如果
ndb_replication
表不存在,则必须创建它。请参阅 ndb_replication 表。在
server_id
列中插入 0 表示访问此表的所有 SQL 节点都应使用冲突解决。如果您只想在特定 mysqld 上使用冲突解决,请使用实际的服务器 ID。在
binlog_type
列中插入NULL
与插入 0 (NBT_DEFAULT
) 的效果相同;使用服务器默认值。创建
test.t1
表CREATE TABLE test.t1 ( columns mycol INT UNSIGNED, columns ) ENGINE=NDB;
现在,当对该表执行更新时,将应用冲突解决,并将具有最大
mycol
值的行版本写入副本。
其他 binlog_type
选项(如 NBT_UPDATED_ONLY_USE_UPDATE
(6
))应使用 ndb_replication
表控制源上的日志记录,而不是使用命令行选项。
NDB$OLD() 示例。 假设正在复制诸如此处定义的 NDB
表,并且您希望为该表的更新启用 “相同时间戳获胜” 冲突解决
CREATE TABLE test.t2 (
a INT UNSIGNED NOT NULL,
b CHAR(25) NOT NULL,
columns,
mycol INT UNSIGNED NOT NULL,
columns,
PRIMARY KEY pk (a, b)
) ENGINE=NDB;
需要按照所示顺序执行以下步骤
首先,在 创建
test.t2
之前,必须在mysql.ndb_replication
表中插入一行,如下所示INSERT INTO mysql.ndb_replication VALUES ('test', 't2', 0, 0, 'NDB$OLD(mycol)');
binlog_type
列的可能值在本节前面已显示;在本例中,我们使用0
指定使用服务器默认日志记录行为。值'NDB$OLD(mycol)'
应插入conflict_fn
列。为
test.t2
创建一个合适的异常表。此处显示的表创建语句包含所有必需的列;任何其他列必须在这些列之后声明,并在表主键定义之前。CREATE TABLE test.t2$EX ( server_id INT UNSIGNED, source_server_id INT UNSIGNED, source_epoch BIGINT UNSIGNED, count INT UNSIGNED, a INT UNSIGNED NOT NULL, b CHAR(25) NOT NULL, [additional_columns,] PRIMARY KEY(server_id, source_server_id, source_epoch, count) ) ENGINE=NDB;
我们可以包含有关给定冲突的类型、原因和源事务 ID 的其他列。我们也不需要为原始表中的所有主键列提供匹配的列。这意味着您可以像这样创建异常表
CREATE TABLE test.t2$EX ( NDB$server_id INT UNSIGNED, NDB$source_server_id INT UNSIGNED, NDB$source_epoch BIGINT UNSIGNED, NDB$count INT UNSIGNED, a INT UNSIGNED NOT NULL, NDB$OP_TYPE ENUM('WRITE_ROW','UPDATE_ROW', 'DELETE_ROW', 'REFRESH_ROW', 'READ_ROW') NOT NULL, NDB$CFT_CAUSE ENUM('ROW_DOES_NOT_EXIST', 'ROW_ALREADY_EXISTS', 'DATA_IN_CONFLICT', 'TRANS_IN_CONFLICT') NOT NULL, NDB$ORIG_TRANSID BIGINT UNSIGNED NOT NULL, [additional_columns,] PRIMARY KEY(NDB$server_id, NDB$source_server_id, NDB$source_epoch, NDB$count) ) ENGINE=NDB;
注意由于我们在表定义中包含了至少一个
NDB$OP_TYPE
、NDB$CFT_CAUSE
或NDB$ORIG_TRANSID
列,因此这四个必需列需要使用NDB$
前缀。创建表
test.t2
,如前所示。
这些步骤必须针对您希望使用 NDB$OLD()
执行冲突解决的每个表执行。对于每个这样的表,必须在 mysql.ndb_replication
中有一行对应的行,并且必须在与正在复制的表相同的数据库中有一个异常表。
读取冲突检测和解决。 NDB Cluster 还支持读取操作的跟踪,这使得在循环复制设置中可以管理集群中给定行的读取与另一个集群中同一行的更新或删除之间的冲突。本示例使用 employee
和 department
表来模拟一种情况,即员工在源集群(我们在此后称为集群 A)中从一个部门转移到另一个部门,而副本集群(在此后称为 B)在交错事务中更新了该员工以前部门的员工人数。
数据表已使用以下 SQL 语句创建
# Employee table
CREATE TABLE employee (
id INT PRIMARY KEY,
name VARCHAR(2000),
dept INT NOT NULL
) ENGINE=NDB;
# Department table
CREATE TABLE department (
id INT PRIMARY KEY,
name VARCHAR(2000),
members INT
) ENGINE=NDB;
两个表的内容包括以下 SELECT
语句的(部分)输出中显示的行
mysql> SELECT id, name, dept FROM employee;
+---------------+------+
| id | name | dept |
+------+--------+------+
...
| 998 | Mike | 3 |
| 999 | Joe | 3 |
| 1000 | Mary | 3 |
...
+------+--------+------+
mysql> SELECT id, name, members FROM department;
+-----+-------------+---------+
| id | name | members |
+-----+-------------+---------+
...
| 3 | Old project | 24 |
...
+-----+-------------+---------+
我们假设我们已经在使用包含四个必需列(这些列用于该表的主键)、操作类型和原因的可选列以及原始表的主键列的异常表,该异常表使用此处显示的 SQL 语句创建
CREATE TABLE employee$EX (
NDB$server_id INT UNSIGNED,
NDB$source_server_id INT UNSIGNED,
NDB$source_epoch BIGINT UNSIGNED,
NDB$count INT UNSIGNED,
NDB$OP_TYPE ENUM( 'WRITE_ROW','UPDATE_ROW', 'DELETE_ROW',
'REFRESH_ROW','READ_ROW') NOT NULL,
NDB$CFT_CAUSE ENUM( 'ROW_DOES_NOT_EXIST',
'ROW_ALREADY_EXISTS',
'DATA_IN_CONFLICT',
'TRANS_IN_CONFLICT') NOT NULL,
id INT NOT NULL,
PRIMARY KEY(NDB$server_id, NDB$source_server_id, NDB$source_epoch, NDB$count)
) ENGINE=NDB;
假设在两个集群上发生两个同时事务。在集群 A 上,我们创建了一个新的部门,然后将员工编号 999 转移到该部门,使用以下 SQL 语句
BEGIN;
INSERT INTO department VALUES (4, "New project", 1);
UPDATE employee SET dept = 4 WHERE id = 999;
COMMIT;
同时,在集群 B 上,另一个事务从 employee
中读取,如下所示
BEGIN;
SELECT name FROM employee WHERE id = 999;
UPDATE department SET members = members - 1 WHERE id = 3;
commit;
冲突事务通常不会被冲突解决机制检测到,因为冲突发生在读取 (SELECT
) 和更新操作之间。您可以通过在副本集群上执行 SET
ndb_log_exclusive_reads
= 1
来规避此问题。以这种方式获取独占读取锁会导致源上读取的任何行被标记为需要在副本集群上进行冲突解决。如果我们在记录这些事务之前以这种方式启用独占读取,则集群 B 上的读取将被跟踪并发送到集群 A 以进行解决;随后会检测到员工行上的冲突,并且集群 B 上的事务将被中止。
冲突在异常表(在集群 A 上)中注册为 READ_ROW
操作(请参阅 冲突解决异常表,了解操作类型的说明),如下所示
mysql> SELECT id, NDB$OP_TYPE, NDB$CFT_CAUSE FROM employee$EX;
+-------+-------------+-------------------+
| id | NDB$OP_TYPE | NDB$CFT_CAUSE |
+-------+-------------+-------------------+
...
| 999 | READ_ROW | TRANS_IN_CONFLICT |
+-------+-------------+-------------------+
读取操作中找到的任何现有行都将被标记。这意味着来自同一冲突的多个行可能会记录在异常表中,如检查来自同一表的集群 A 上的更新与集群 B 上的多个行的读取之间的冲突的影响所表明的那样,这些冲突在同时事务中发生。在集群 A 上执行的事务如下所示
BEGIN;
INSERT INTO department VALUES (4, "New project", 0);
UPDATE employee SET dept = 4 WHERE dept = 3;
SELECT COUNT(*) INTO @count FROM employee WHERE dept = 4;
UPDATE department SET members = @count WHERE id = 4;
COMMIT;
同时,包含此处显示的语句的事务在集群 B 上运行
SET ndb_log_exclusive_reads = 1; # Must be set if not already enabled
...
BEGIN;
SELECT COUNT(*) INTO @count FROM employee WHERE dept = 3 FOR UPDATE;
UPDATE department SET members = @count WHERE id = 3;
COMMIT;
在本例中,所有与第二个事务的 SELECT
中的 WHERE
条件匹配的三个行都将被读取,因此在异常表中被标记,如下所示
mysql> SELECT id, NDB$OP_TYPE, NDB$CFT_CAUSE FROM employee$EX;
+-------+-------------+-------------------+
| id | NDB$OP_TYPE | NDB$CFT_CAUSE |
+-------+-------------+-------------------+
...
| 998 | READ_ROW | TRANS_IN_CONFLICT |
| 999 | READ_ROW | TRANS_IN_CONFLICT |
| 1000 | READ_ROW | TRANS_IN_CONFLICT |
...
+-------+-------------+-------------------+
读取跟踪是在现有行的基础上执行的。基于给定条件的读取只会跟踪任何 找到 的行的冲突,而不会跟踪在交错事务中插入的任何行的冲突。这与在 NDB Cluster 的单个实例中执行独占行锁定类似。
插入冲突检测和解决示例。 以下示例说明了插入冲突检测函数的使用。我们假设我们正在复制数据库 test
中的两个表 t1
和 t2
,并且我们希望对 t1
使用带有 NDB$MAX_INS()
的插入冲突检测,对 t2
使用带有 NDB$MAX_DEL_WIN_INS()
的插入冲突检测。这两个数据表直到设置过程的后期才会创建。
设置插入冲突解决类似于设置先前示例中显示的其他冲突检测和解决算法。如果用于配置二进制日志记录和冲突解决的 mysql.ndb_replication
表不存在,则首先需要创建它,如下所示
CREATE TABLE mysql.ndb_replication (
db VARBINARY(63),
table_name VARBINARY(63),
server_id INT UNSIGNED,
binlog_type INT UNSIGNED,
conflict_fn VARBINARY(128),
PRIMARY KEY USING HASH (db, table_name, server_id)
) ENGINE=NDB
PARTITION BY KEY(db,table_name);
ndb_replication
表以每个表的为基础执行操作;也就是说,我们需要插入一行,其中包含表信息、binlog_type
值、要使用的冲突解决函数以及每个要设置的表的时区列 (X
) 的名称,如下所示
INSERT INTO mysql.ndb_replication VALUES ("test", "t1", 0, 7, "NDB$MAX_INS(X)");
INSERT INTO mysql.ndb_replication VALUES ("test", "t2", 0, 7, "NDB$MAX_DEL_WIN_INS(X)");
这里我们将 binlog_type
设置为 NBT_FULL_USE_UPDATE
(7
),这意味着始终记录完整行。请参阅 ndb_replication 表,了解其他可能的值。
您还可以为每个要使用冲突解决的 NDB
表创建相应的异常表。异常表记录了给定表中被冲突解决函数拒绝的所有行。可以使用以下两个 SQL 语句为表 t1
和 t2
的复制冲突检测创建异常表
CREATE TABLE `t1$EX` (
NDB$server_id INT UNSIGNED,
NDB$source_server_id INT UNSIGNED,
NDB$source_epoch BIGINT UNSIGNED,
NDB$count INT UNSIGNED,
NDB$OP_TYPE ENUM('WRITE_ROW', 'UPDATE_ROW', 'DELETE_ROW',
'REFRESH_ROW', 'READ_ROW') NOT NULL,
NDB$CFT_CAUSE ENUM('ROW_DOES_NOT_EXIST', 'ROW_ALREADY_EXISTS',
'DATA_IN_CONFLICT', 'TRANS_IN_CONFLICT') NOT NULL,
a INT NOT NULL,
PRIMARY KEY(NDB$server_id, NDB$source_server_id,
NDB$source_epoch, NDB$count)
) ENGINE=NDB;
CREATE TABLE `t2$EX` (
NDB$server_id INT UNSIGNED,
NDB$source_server_id INT UNSIGNED,
NDB$source_epoch BIGINT UNSIGNED,
NDB$count INT UNSIGNED,
NDB$OP_TYPE ENUM('WRITE_ROW', 'UPDATE_ROW', 'DELETE_ROW',
'REFRESH_ROW', 'READ_ROW') NOT NULL,
NDB$CFT_CAUSE ENUM( 'ROW_DOES_NOT_EXIST', 'ROW_ALREADY_EXISTS',
'DATA_IN_CONFLICT', 'TRANS_IN_CONFLICT') NOT NULL,
a INT NOT NULL,
PRIMARY KEY(NDB$server_id, NDB$source_server_id,
NDB$source_epoch, NDB$count)
) ENGINE=NDB;
最后,在创建了刚才显示的异常表之后,您可以使用以下两个 SQL 语句创建要复制并受冲突解决控制的数据表
CREATE TABLE t1 (
a INT PRIMARY KEY,
b VARCHAR(32),
X INT UNSIGNED
) ENGINE=NDB;
CREATE TABLE t2 (
a INT PRIMARY KEY,
b VARCHAR(32),
X INT UNSIGNED
) ENGINE=NDB;
对于每个表,X
列用作时间戳列。
一旦在源上创建,t1
和 t2
就会被复制,并可以假定存在于源和副本上。在本例的其余部分,我们使用 mysqlS>
来表示连接到源的 mysql 客户端,并使用 mysqlR>
来表示在副本上运行的 mysql 客户端。
首先,我们在源上的表中分别插入一行,如下所示
mysqlS> INSERT INTO t1 VALUES (1, 'Initial X=1', 1);
Query OK, 1 row affected (0.01 sec)
mysqlS> INSERT INTO t2 VALUES (1, 'Initial X=1', 1);
Query OK, 1 row affected (0.01 sec)
我们可以确定这两个行被复制而不会导致任何冲突,因为副本上的表在源上发出 INSERT
语句之前不包含任何行。我们可以通过从副本上的表中选择来验证这一点,如下所示
mysqlR> TABLE t1 ORDER BY a;
+---+-------------+------+
| a | b | X |
+---+-------------+------+
| 1 | Initial X=1 | 1 |
+---+-------------+------+
1 row in set (0.00 sec)
mysqlR> TABLE t2 ORDER BY a;
+---+-------------+------+
| a | b | X |
+---+-------------+------+
| 1 | Initial X=1 | 1 |
+---+-------------+------+
1 row in set (0.00 sec)
接下来,我们在副本上的表中插入新行,如下所示
mysqlR> INSERT INTO t1 VALUES (2, 'Replica X=2', 2);
Query OK, 1 row affected (0.01 sec)
mysqlR> INSERT INTO t2 VALUES (2, 'Replica X=2', 2);
Query OK, 1 row affected (0.01 sec)
现在,我们在源上的表中插入冲突的行,这些行具有更大的时间戳 (X
) 列值,使用此处显示的语句
mysqlS> INSERT INTO t1 VALUES (2, 'Replica X=20', 20);
Query OK, 1 row affected (0.01 sec)
mysqlS> INSERT INTO t2 VALUES (2, 'Replica X=20', 20);
Query OK, 1 row affected (0.01 sec)
现在,我们通过从副本上的两个表中选择(再次)来观察结果,如下所示
mysqlR> TABLE t1 ORDER BY a;
+---+-------------+-------+
| a | b | X |
+---+-------------+-------+
| 1 | Initial X=1 | 1 |
+---+-------------+-------+
| 2 | Source X=20 | 20 |
+---+-------------+-------+
2 rows in set (0.00 sec)
mysqlR> TABLE t2 ORDER BY a;
+---+-------------+-------+
| a | b | X |
+---+-------------+-------+
| 1 | Initial X=1 | 1 |
+---+-------------+-------+
| 1 | Source X=20 | 20 |
+---+-------------+-------+
2 rows in set (0.00 sec)
在源上插入的行,其时间戳大于副本上冲突行中的时间戳,已替换了这些行。在副本上,我们接下来插入两个新行,它们与 t1
或 t2
中的任何现有行都不冲突,如下所示
mysqlR> INSERT INTO t1 VALUES (3, 'Replica X=30', 30);
Query OK, 1 row affected (0.01 sec)
mysqlR> INSERT INTO t2 VALUES (3, 'Replica X=30', 30);
Query OK, 1 row affected (0.01 sec)
在源上插入更多具有相同主键值 (3
) 的行会导致与之前相同的冲突,但这次我们使用的时间戳列的值小于副本上冲突行中同一列的值。
mysqlS> INSERT INTO t1 VALUES (3, 'Source X=3', 3);
Query OK, 1 row affected (0.01 sec)
mysqlS> INSERT INTO t2 VALUES (3, 'Source X=3', 3);
Query OK, 1 row affected (0.01 sec)
我们可以通过查询表看到,来自源的两个插入都被副本拒绝,并且之前在副本上插入的行没有被覆盖,如副本上 mysql 客户端中所示
mysqlR> TABLE t1 ORDER BY a;
+---+--------------+-------+
| a | b | X |
+---+--------------+-------+
| 1 | Initial X=1 | 1 |
+---+--------------+-------+
| 2 | Source X=20 | 20 |
+---+--------------+-------+
| 3 | Replica X=30 | 30 |
+---+--------------+-------+
3 rows in set (0.00 sec)
mysqlR> TABLE t2 ORDER BY a;
+---+--------------+-------+
| a | b | X |
+---+--------------+-------+
| 1 | Initial X=1 | 1 |
+---+--------------+-------+
| 2 | Source X=20 | 20 |
+---+--------------+-------+
| 3 | Replica X=30 | 30 |
+---+--------------+-------+
3 rows in set (0.00 sec)
您可以在异常表中看到有关被拒绝行的信息,如下所示
mysqlR> SELECT NDB$server_id, NDB$source_server_id, NDB$count,
> NDB$OP_TYPE, NDB$CFT_CAUSE, a
> FROM t1$EX
> ORDER BY NDB$count\G
*************************** 1. row ***************************
NDB$server_id : 2
NDB$source_server_id: 1
NDB$count : 1
NDB$OP_TYPE : WRITE_ROW
NDB$CFT_CAUSE : DATA_IN_CONFLICT
a : 3
1 row in set (0.00 sec)
mysqlR> SELECT NDB$server_id, NDB$source_server_id, NDB$count,
> NDB$OP_TYPE, NDB$CFT_CAUSE, a
> FROM t2$EX
> ORDER BY NDB$count\G
*************************** 1. row ***************************
NDB$server_id : 2
NDB$source_server_id: 1
NDB$count : 1
NDB$OP_TYPE : WRITE_ROW
NDB$CFT_CAUSE : DATA_IN_CONFLICT
a : 3
1 row in set (0.00 sec)
正如我们之前所见,在源上插入的其他行都没有被副本拒绝,只有那些时间戳值小于副本上冲突行的时间戳值的行。