21.6.11 NDB 群集复制冲突解决

当使用涉及多个源的复制设置(包括循环复制)时,不同的源可能会尝试使用不同的数据更新副本上的同一行。 NDB 群集复制中的冲突解决方案提供了一种解决此类冲突的方法,方法是允许使用用户定义的解决方案列来确定是否应将给定源的更新应用于副本。

NDB 群集支持的某些类型的冲突解决方案(NDB$OLD()NDB$MAX()NDB$MAX_DELETE_WIN())将此用户定义的列实现为“时间戳”列(尽管其类型不能为TIMESTAMP,如本节稍后所述)。这些类型的冲突解决方案始终是逐行而不是事务性地应用。基于纪元的冲突解决函数NDB$EPOCH()NDB$EPOCH_TRANS()比较纪元的复制 Sequences(因此这些函数是事务性的)。发生冲突时,可以使用不同的方法比较副本上的分辨率列值,如本节后面所述;可以按 table 设置使用的方法。

您还应牢记,应用程序有责任确保使用相关值正确填充“分辨率”列,以便在确定是否应用更新时分辨率功能可以做出适当的选择。

要求. 必须在源和副本上都准备好解决冲突的方法。下面的列 table 中描述了这些任务:

  • 在编写二进制日志的源代码上,您必须确定要发送的列(所有列或仅更新的列)。通过应用mysqld启动选项--ndb-log-updated-only(在本节中稍后介绍),或者在每个 table 的基础上,通过mysql.ndb_replicationtable 中的条目(请参见ndb_replication 系统 table),对于整个 MySQL Server 而言都是如此。

Note

如果要复制具有很大列(例如TEXTBLOB列)的 table,则--ndb-log-updated-only还可用于减小二进制日志的大小,并避免由于超过max_allowed_packet而导致的复制失败。

有关此问题的更多信息,请参见第 16.4.1.19 节,“复制和 max_allowed_packet”

  • 在副本服务器上,您必须确定要应用的冲突解决类型(“最新时间戳获胜”,“相同时间戳获胜”,“主要胜利”,“主要胜利,完成 Transaction”或无)。这是根据每个 table 使用mysql.ndb_replication系统 table 完成的(请参见ndb_replication 系统 table)。

  • NDB 群集还支持读取冲突检测,即检测一个群集中给定行的读取与另一群集中同一行的更新或删除之间的冲突。这需要通过在副本上将ndb_log_exclusive_reads设置为 1 所获得的互斥读取锁定。冲突读取读取的所有行均记录在 exceptiontable 中。有关更多信息,请参见读取冲突检测和解决

当使用函数NDB$OLD()NDB$MAX()NDB$MAX_DELETE_WIN()进行基于时间戳的冲突解决时,我们通常将用于确定更新的列称为“时间戳”列。但是,此列的数据类型永远不会为TIMESTAMP;相反,其数据类型应为INT(INTEGER)或BIGINT。 “时间戳记”列也应为UNSIGNEDNOT NULL

本节稍后讨论的NDB$EPOCH()NDB$EPOCH_TRANS()函数通过比较应用于主要和辅助 NDB 群集的复制时期的相对 Sequences 来工作,并且不使用时间戳。

源列控件. 我们可以按照“之前”和“之后”图像查看更新操作-即应用更新前后的 table 状态。通常,当使用主键更新 table 时,“前”图像不是很重要;但是,当我们需要根据每次更新确定是否在副本上使用更新的值时,我们需要确保将两个映像都写入源的二进制日志中。如本节稍后所述,使用mysqld--ndb-log-update-as-write选项可以完成此操作。

Important

启动 MySQL 服务器时决定是否记录完整的行还是仅记录更新的列,并且不能在线更改。您必须重新启动mysqld或使用不同的日志记录选项启动一个新的mysqld实例。

记录全部或部分行(--ndb-log-updated-only 选项)

PropertyValue
Command-Line Format--ndb-log-updated-only[={OFF|ON}]
System Variablendb_log_updated_only
ScopeGlobal
DynamicYes
TypeBoolean
Default ValueON

为了解决冲突,有两种记录行的基本方法,具体取决于mysqld--ndb-log-updated-only选项的设置:

  • 记录完整的行

  • 仅记录已更新的列数据,即已设置其值的列数据,而不管此值是否实际更改。这是默认行为。

通常只记录更新的列就足够了,而且效率更高。但是,如果需要记录完整的行,可以通过将--ndb-log-updated-only设置为0OFF来进行记录。

--ndb-log-update-as-write 选项:将更改的数据记录为更新

PropertyValue
Command-Line Format--ndb-log-update-as-write[={OFF|ON}]
System Variablendb_log_update_as_write
ScopeGlobal
DynamicYes
TypeBoolean
Default ValueON

MySQL Server 的--ndb-log-update-as-write选项的设置确定是否使用“之前”映像执行日志记录。因为冲突解决是在 MySQL Server 的更新处理程序中完成的,所以有必要控制复制源执行的日志记录,以使更新是更新而不是写入。也就是说,即使更新取代了现有行,更新也被视为现有行中的更改,而不是写入新行。默认情况下,此选项处于打开状态。换句话说,更新被视为写入。也就是说,默认情况下,更新在二进制日志中写为write_row事件,而不是update_row事件。

要禁用该选项,请使用--ndb-log-update-as-write=0--ndb-log-update-as-write=OFF启动源mysqld。从 NDBtable 复制到使用其他存储引擎的 table 时,必须执行此操作。有关更多信息,请参见从 NDB 复制到其他存储引擎从 NDB 复制到非事务存储引擎

冲突解决控制. 通常在可能发生冲突的服务器上启用冲突解决。与日志记录方法选择一样,它由mysql.ndb_replicationtable 中的条目启用。

ndb_replication 系统 table. 要启用冲突解决,必须根据冲突解决类型和要使用的方法在mysql系统数据库上的源,副本或两者上创建ndb_replicationtable。受雇。该 table 用于按 table 控制日志记录和冲突解决功能,并且每个 table 在复制中涉及一行。在要解决冲突的服务器上创建ndb_replication并在其中填充控制信息。在简单的源副本设置中,也可以在副本上本地更改数据,通常是副本。在更复杂的复制方案(如双向复制)中,通常是所有涉及的源。 mysql.ndb_replication中的每一行都对应于一个要复制的 table,并指定如何记录和解决该 table 的冲突(即使用哪个冲突解决功能,如果有的话)。 mysql.ndb_replicationtable 的定义如下所示:

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);

该 table 中的列在接下来的几段中进行描述。

db. 包含要复制的 table 的数据库的名称。您可以使用通配符_%之一或全部作为数据库名称的一部分。匹配类似于为LIKE运算符实施的匹配。

table_name. 要复制的 table 的名称。table 名可以包含通配符_%之一或全部。匹配类似于为LIKE运算符实施的匹配。

server_id. table 格所在的 MySQL 实例(SQL 节点)的唯一服务器 ID。

binlog_type. 要使用的二进制日志记录的类型。如下 table 所示确定:

table21.284 binlog_type 值以及内部值和说明

ValueInternal ValueDescription
0NBT_DEFAULT使用服务器默认值
1NBT_NO_LOGGING不要将此 table 记录在二进制日志中
2NBT_UPDATED_ONLY仅记录更新的属性
3NBT_FULL记录完整的行,即使未更新(MySQL 服务器默认行为)
4NBT_USE_UPDATE(仅用于生成NBT_UPDATED_ONLY_USE_UPDATENBT_FULL_USE_UPDATE值-不能单独使用)
5[ Not used ]---
6NBT_UPDATED_ONLY_USE_UPDATE(等于NBT_UPDATED_ONLY | NBT_USE_UPDATE)使用更新的属性,即使值不变
7NBT_FULL_USE_UPDATE(等于NBT_FULL | NBT_USE_UPDATE)即使值不变,也使用整行

conflict_fn. 要应用的冲突解决功能。必须将此功能指定为以下列 table 中显示的功能之一:

在接下来的几段中将介绍这些功能。

NDB $ OLD(column_name). 如果源和副本中* column_name *的值相同,那么将应用更新;否则,将应用更新。否则,更新将不会应用于副本,并且会将异常写入日志。以下伪代码对此进行了说明:

if (source_old_column_value == replica_current_column_value)
  apply_update();
else
  log_exception();

此功能可用于“相同的价值取胜”冲突解决方案。这种类型的冲突解决方案可确保不会将错误来源的更新应用于副本。

Important

此功能使用源“之前”图像中的列值。

NDB $ MAX(column_name). 如果来自源的给定行的“时间戳”列值高于副本数据库上的值,则将应用该列;否则,它不会应用于副本。以下伪代码对此进行了说明:

if (source_new_column_value > replica_current_column_value)
  apply_update();

此功能可用于“最大时间戳获胜”冲突解决。这种类型的冲突解决方案确保在发生冲突的情况下,最近更新的行的版本是持久的版本。

Important

此功能使用源“后”图像中的列值。

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();

此功能可用于“最大时间戳,删除获胜”冲突解决。这种类型的冲突解决方案确保在发生冲突的情况下,已删除或(否则)最近更新的行的版本是持久的版本。

Note

NDB$MAX()一样,源“后”图像中的列值就是此函数使用的值。

NDB $ EPOCH()和 NDB $ EPOCH_TRANS(). NDB$EPOCH()函数跟踪相对于源自副本的更改在副本群集上应用复制纪元的 Sequences。此相对 Sequences 用于确定源自副本的更改是否与本地发起的任何更改并发,因此有可能发生冲突。

NDB$EPOCH()描述中的大部分内容也适用于NDB$EPOCH_TRANS()。文本中注明了任何 exceptions。

NDB$EPOCH()是非对称的,在双向复制配置(有时称为“主动-主动”复制)中的一个 NDB 群集上运行。我们在这里指的是集群,集群作为主集群,另一个作为辅助集群。主节点上的副本负责检测和处理冲突,而辅助节点上的副本不参与任何冲突的检测或处理。

当主数据库上的副本检测到冲突时,它将事件注入其自己的二进制日志中以弥补这些冲突。这样可以确保辅助 NDB 群集最终与主数据库重新对齐,从而防止主数据库和辅助数据库分离。这种补偿和重新调整机制要求主 NDB 群集始终赢得与辅助数据库的任何冲突,也就是说,在发生冲突时,始终使用主数据库的更改而不是辅助数据库的更改。此“主要总是胜利”规则具有以下含义:

  • 一旦提交到主数据库,更改数据的操作将完全持久,不会因冲突检测和解决而撤消或回滚。

  • 从主数据库读取的数据是完全一致的。提交到主服务器上的任何更改(本地或从副本)都不会在以后还原。

  • 如果主服务器确定发生冲突,则更改辅助服务器上的数据的操作以后可以还原。

  • 在辅助节点上读取的各个行始终保持一致,每一行始终反映辅助节点提交的状态或主要节点提交的状态。

  • 在辅助节点上读取的行集在给定的单个时间点可能不一定一致。对于NDB$EPOCH_TRANS(),这是一个瞬态;对于NDB$EPOCH(),它可以是持久状态。

  • 假设有一段足够长的时间而没有任何冲突,则辅助 NDB 群集上的所有数据(最终)将与主要数据一致。

NDB$EPOCH()NDB$EPOCH_TRANS()不需要任何用户架构修改,也不需要更改应用程序即可提供冲突检测。但是,必须仔细考虑所使用的架构和所使用的访问模式,以验证整个系统在指定的限制内运行。

NDB$EPOCH()NDB$EPOCH_TRANS()函数中的每一个都可以采用可选参数;这是用于 table 示时代的低 32 位的位数,并且应设置为不小于计算出的值,如下所示:

CEIL( LOG2( TimeBetweenGlobalCheckpoints / TimeBetweenEpochs ), 1)

对于这些配置参数的默认值(分别为 2000 和 100 毫秒),它的值为 5 位,因此默认值(6)应该足够,除非将其他值用于TimeBetweenGlobalCheckpointsTimeBetweenEpochs或两者都使用。太小的值可能导致误报,而太大的值可能导致数据库浪费过多的空间。

NDB$EPOCH()NDB$EPOCH_TRANS()都将有冲突的行的条目插入到相关的 exceptiontable 中,前提是这些 table 是根据与本节其他地方所述的相同的 exceptiontable 架构规则定义的(请参见NDB$OLD(column_name))。您必须先创建任何 exceptiontable,然后才能创建要使用它的数据 table。

与本节讨论的其他冲突检测功能一样,通过在mysql.ndb_replicationtable 中包含相关条目来激活NDB$EPOCH()NDB$EPOCH_TRANS()(请参见ndb_replication 系统 table)。在此方案中,主要和辅助 NDB 群集的角色完全由mysql.ndb_replicationtable 条目确定。

由于NDB$EPOCH()NDB$EPOCH_TRANS()采用的冲突检测算法是不对称的,因此必须对主副本和辅助副本的server_id条目使用不同的值。

DELETE个操作之间的冲突不足以触发使用NDB$EPOCH()NDB$EPOCH_TRANS()的冲突,并且历元内的相对位置无关紧要。 (缺陷号 18459944)

冲突检测状态变量. 几个状态变量可用于监视冲突检测。您可以看到NDB$EPOCH()自上次从Ndb_conflict_fn_epoch系统状态变量的当前值重新启动该副本以来,发现有多少行发生冲突。

Ndb_conflict_fn_epoch_trans提供NDB$EPOCH_TRANS()直接发现冲突的行数。 Ndb_conflict_fn_epoch2Ndb_conflict_fn_epoch2_trans分别显示由NDB$EPOCH2()NDB$EPOCH2_TRANS()冲突的行数。实际重新对齐的行数(包括因与其他冲突行的成员身份或对相同事务的依赖性而受影响的行)由Ndb_conflict_trans_row_reject_count给出。

有关更多信息,请参见第 21.3.3.9.3 节“ NDB 群集状态变量”

对 NDB $ EPOCH()的限制. 使用NDB$EPOCH()执行冲突检测时,当前存在以下限制:

  • 使用 NDB 群集纪元边界检测冲突,其粒度与TimeBetweenEpochs成正比(默认值:100 毫秒)。最小冲突窗口是两个群集上对同一数据的并发更新始终报告冲突的最短时间。这始终是非零的时间长度,并且大致与2 * (latency + queueing + TimeBetweenEpochs)成正比。这意味着-假设TimeBetweenEpochs为默认值,并且忽略群集之间的任何延迟(以及任何排队延迟)-最小冲突窗口大小约为 200 毫秒。在查看预期的应用程序“竞赛”模式时,应考虑此最小窗口。

  • 使用NDB$EPOCH()NDB$EPOCH_TRANS()函数的 table 需要额外的存储空间;每行需要 1 到 32 位额外的空间,具体取决于传递给函数的值。

  • 删除操作之间的冲突可能会导致主要和次要之间的分歧。同时删除两个群集上的行时,可以检测到冲突,但不会记录冲突,因为该行已删除。这意味着在后续任何重新对齐操作的传播过程中都不会检测到其他冲突,这可能会导致分歧。

删除应从外部进行序列化,或仅路由到一个群集。或者,应使用此类删除及其后的任何插入内容在事务上更新单独的行,以便可以在行删除之间跟踪冲突。这可能需要更改应用程序。

  • 使用NDB$EPOCH()NDB$EPOCH_TRANS()进行冲突检测时,当前仅支持双向“主动-主动”配置中的两个 NDB 群集。

  • NDB$EPOCH()NDB$EPOCH_TRANS()当前不支持具有BLOBTEXT列的 table。

NDB $ EPOCH_TRANS(). NDB$EPOCH_TRANS()扩展了NDB$EPOCH()函数。使用“主要胜出”规则(请参见NDB $ EPOCH()和 NDB $ EPOCH_TRANS())以相同的方式检测和处理冲突,但额外的条件是,在发生冲突的同一事务中更新的任何其他行也都被视为冲突。换句话说,NDB$EPOCH()重新对齐辅助节点上的各个冲突行,NDB$EPOCH_TRANS()重新对齐冲突的事务。

另外,任何可检测地依赖于冲突事务的事务也都被视为冲突,这些依赖关系由辅助群集的二进制日志的内容确定。由于二进制日志仅包含数据修改操作(插入,更新和删除),因此仅使用重叠的数据修改来确定事务之间的依赖关系。

NDB$EPOCH_TRANS()受与NDB$EPOCH()相同的条件和限制,此外还要求使用版本 2 二进制日志行事件(log_bin_use_v1_row_events等于 0),这会在二进制日志中增加每个事件 2 个字节的存储开销。此外,所有事务 ID 必须记录在辅助数据库的二进制日志中(--ndb-log-transaction-id选项),这会增加额外的可变开销(每行最多 13 个字节)。

See NDB $ EPOCH()和 NDB $ EPOCH_TRANS().

状态信息. 服务器状态变量Ndb_conflict_fn_max提供自上次启动mysqld以来由于“最大时间戳获胜”冲突解决而未将行应用于当前 SQL 节点的次数。

自上次重启以来,由于给定的mysqld上的“相同的时间戳获胜”冲突解决而没有应用行的次数由全局状态变量Ndb_conflict_fn_old给出。除了增加Ndb_conflict_fn_old之外,未使用的行的主键还会插入到异常 table 中,这将在本节后面部分进行说明。

NDB $ EPOCH2(). NDB$EPOCH2()功能类似于NDB$EPOCH(),不同之处在于NDB$EPOCH2()提供了双向复制拓扑的删除-删除处理。在这种情况下,通过将ndb_slave_conflict_role系统变量设置为每个源上的适当值(通常是PRIMARYSECONDARY中的一个),将主要角色和次要角色分配给两个来源。完成此操作后,辅助服务器所做的修改将由主服务器反映回辅助服务器,然后有条件地应用它们。

NDB $ EPOCH2_TRANS(). NDB$EPOCH2_TRANS()扩展了NDB$EPOCH2()函数。以相同的方式检测和处理冲突,并将主要角色和次要角色分配给复制群集,但额外的条件是,在发生冲突的同一事务中更新的任何其他行也被视为存在冲突。也就是说,NDB$EPOCH2()重新对齐辅助节点上的冲突行,而NDB$EPOCH_TRANS()重新对齐冲突的事务。

其中NDB$EPOCH()NDB$EPOCH_TRANS()使用在最后一次修改的纪元中为每行指定的元数据,以在主数据库上确定从辅助数据库传入的复制行更改是否与本地提交的更改同时发生;并发更改被认为是冲突的,随后的异常 table 更新和辅助节点的重新对齐。当在主数据库上删除行时会出现问题,因此不再有任何最后修改的时期可用于确定任何复制的操作是否发生冲突,这意味着未检测到冲突的删除操作。这可能会导致分歧,例如一个群集上的删除与另一群集上的删除并发同时进行;这就是为什么使用NDB$EPOCH()NDB$EPOCH_TRANS()时删除操作只能路由到一个群集的原因。

NDB$EPOCH2()通过忽略任何删除-删除冲突并避免任何潜在的结果分歧,从而绕过上述问题(在 PRIMARY 上存储有关已删除行的信息)。这是通过反映成功应用到辅助节点并从辅助节点复制回辅助节点的任何操作来完成的。当它返回到辅助服务器时,可以用来在辅助服务器上重新应用操作,该操作已被来自主服务器的操作删除。

使用NDB$EPOCH2()时,应牢记辅助节点从主节点应用删除,删除新行,直到通过反射操作将其恢复。从理论上讲,辅助节点上的后续插入或更新与从主节点上的删除冲突,但是在这种情况下,我们选择忽略此操作,并允许辅助节点“获胜”,以防止集群之间的分歧。换句话说,删除后,主数据库不会检测到冲突,而是立即采用辅助数据库的以下更改。因此,从属服务器的状态可以在进入最终(稳定)状态时重新访问多个先前提交的状态,并且其中一些可能是可见的。

您还应该意识到,将所有从辅助服务器回到主服务器的操作反映出来,会增加主服务器日志二进制日志的大小,以及对带宽,CPU 使用率和磁盘 I/O 的要求。

在辅助服务器上应用反射操作取决于辅助服务器上目标行的状态。可以通过检查Ndb_conflict_reflected_op_prepare_countNdb_conflict_reflected_op_discard_count状态变量来跟踪是否将反映的更改应用于辅助服务器。应用的更改次数仅仅是这两个值之间的差(请注意Ndb_conflict_reflected_op_prepare_count始终大于或等于Ndb_conflict_reflected_op_discard_count)。

当且仅当同时满足以下两个条件时,才应用事件:

  • 该行的存在(即,是否存在)与事件的类型一致。对于删除和更新操作,该行必须已经存在。对于插入操作,该行必须存在。

  • 该行最后被主数据库修改。通过执行反射操作可以完成修改。

如果两个条件都不同时满足,则反射操作将被次级服务器丢弃。

冲突解决 exceptiontable. 要使用NDB$OLD()冲突解决功能,还必须创建一个与每个NDBtable 相对应的 exceptiontable,为此要使用这种类型的冲突解决方法。使用NDB$EPOCH()NDB$EPOCH_TRANS()时也是如此。该 table 的名称是要应用冲突解决方案的 table 的名称,后接字符串$EX。 (例如,如果原始 table 的名称为mytable,则相应的 exceptiontable 名称应为mytable$EX.)创建 exceptiontable 的语法如下所示:

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;

前四列是必需的。前四列的名称以及与原始 table 的主键列匹配的列的名称并不重要;但是,出于清晰和一致性的考虑,我们建议您将此处显示的名称用于server_idsource_server_idsource_epochcount列,并将与原始 table 中相同的名称用于与原始 table 中相同的列 table 的主键。

如果 exceptiontable 使用本节稍后讨论的一个或多个可选列NDB$OP_TYPENDB$CFT_CAUSENDB$ORIG_TRANSID,则每个必需列也必须使用前缀NDB$命名。如果需要,即使未定义任何可选列,也可以使用NDB$前缀命名所需的列,但是在这种情况下,必须使用前缀来命名所有四个必需列。

在这些列之后,应按用于定义原始 table 主键的 Sequences 复制构成原始 table 主键的列。复制原始 table 的主键列的列的数据类型应与原始列的数据类型相同(或大于原始列的数据类型)。可以使用主键列的子集。

exceptiontable 必须使用NDB存储引擎。 (本节后面的示例显示了将NDB$OLD()与 exceptiontable 一起使用的示例.)

可以在复制的主键列之后定义其他列,但不能在其中任何一个之前定义;任何此类额外的列都不能为NOT NULL。 NDB Cluster 支持三个附加的 sched 义的可选列NDB$OP_TYPENDB$CFT_CAUSENDB$ORIG_TRANSID,这些将在接下来的几段中进行介绍。

NDB$OP_TYPE:此列可用于获取导致冲突的操作类型。如果使用此列,请按如下所示对其进行定义:

NDB$OP_TYPE ENUM('WRITE_ROW', 'UPDATE_ROW', 'DELETE_ROW',
    'REFRESH_ROW', 'READ_ROW') NOT NULL

WRITE_ROWUPDATE_ROWDELETE_ROW操作类型 table 示用户启动的操作。 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_ROWWRITE_ROW操作的原因; WRITE_ROW事件可以报告ROW_ALREADY_EXISTS。当基于行的冲突函数检测到冲突时报告DATA_IN_CONFLICT;当事务冲突函数拒绝属于一个完整事务的所有操作时,报告TRANS_IN_CONFLICT

NDB$ORIG_TRANSIDNDB$ORIG_TRANSID列(如果使用)包含原始事务的 ID。该列的定义如下:

NDB$ORIG_TRANSID BIGINT UNSIGNED NOT NULL

NDB$ORIG_TRANSIDNDB生成的 64 位值。此值可用于关联来自相同或不同异常 table 的属于同一冲突事务的多个异常 table 条目。

不属于原始 table 主键的其他参考列可以命名为colname$OLDcolname$NEWcolname$OLD在更新和删除操作(即包含DELETE_ROW事件的操作)中引用旧值。 colname$NEW可用于在插入和更新操作(即使用WRITE_ROW事件,UPDATE_ROW事件或两种类型的事件的操作)中引用新值。如果有冲突的操作没有为给定的参考列提供不是主键的值,则 exceptiontable 行包含NULL或该列的定义默认值。

Important

设置数据 table 以进行复制时,将读取mysql.ndb_replicationtable,因此必须在创建要复制的 table 之前*之前将与要复制的 table 相对应的行插入mysql.ndb_replication中。

Examples

以下示例假定您已经具有工作的 NDB 群集复制设置,如第 21.6.5 节“准备要复制的 NDB 群集”第 21.6.6 节“启动 NDB 群集复制(单个复制通道)”中所述。

NDB $ MAX()示例. 假设您希望使用列mycol作为“时间戳”在 tabletest.t1上启用“最大时间戳获胜”冲突解决方案。可以使用以下步骤完成此操作:

INSERT INTO mysql.ndb_replication
    VALUES ('test', 't1', 0, NULL, 'NDB$MAX(mycol)');

server_id中插入 0table 示所有访问此 table 的 SQL 节点都应使用冲突解决方案。如果只想在特定的mysqld上使用冲突解决方案,请使用实际的服务器 ID。

binlog_type列中插入NULL与插入 0(NBT_DEFAULT)具有相同的效果;使用服务器默认值。

  • 创建test.t1table:
CREATE TABLE test.t1 (
    columns
    mycol INT UNSIGNED,
    columns
) ENGINE=NDB;

现在,当对此 table 执行更新时,将应用冲突解决方案,并将具有最大mycol值的行的版本写入副本。

Note

其他binlog_type选项(例如NBT_UPDATED_ONLY_USE_UPDATE)应用于通过ndb_replicationtable 而不是命令行选项来控制对源的日志记录。

NDB $ OLD()示例. 假设正在复制NDBtable(例如此处定义的 table),并且您希望启用“相同的时间戳获胜”冲突解决方案来更新此 table:

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;

需要按照显示的 Sequences 执行以下步骤:

  • 首先(也是在创建test.t2之前),您必须在mysql.ndb_replicationtable 中插入一行,如下所示:
INSERT INTO mysql.ndb_replication
    VALUES ('test', 't2', 0, NULL, 'NDB$OLD(mycol)');

binlog_type列的可能值在本节的前面显示。值'NDB$OLD(mycol)'应该插入到conflict_fn列中。

  • test.t2创建一个适当的 exceptiontable。此处显示的 table 创建语句包括所有必需的列。必须在这些列之后,table 的主键定义之前声明任何其他列。
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 的信息。我们也不需要为原始 table 中的所有主键列提供匹配的列。这意味着您可以这样创建 exceptiontable:

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;

Note

由于我们在 table 定义中至少包含了NDB$OP_TYPENDB$CFT_CAUSENDB$ORIG_TRANSID列之一,因此这四个必填列都需要NDB$前缀。

  • 如上所示创建 tabletest.t2

对于要使用NDB$OLD()执行冲突解决的每个 table,必须遵循这些步骤。对于每个这样的 table,mysql.ndb_replication中必须有一个对应的行,并且与要复制的 table 相同的数据库中必须有一个 exceptiontable。

读取冲突检测和解决. NDB Cluster 还支持跟踪读取操作,这使得在循环复制设置中可以 Management 一个群集中给定行的读取与另一群集中同一行的更新或删除之间的冲突。 。本示例使用employeedepartmenttable 来建模一个场景,在该场景中,员工在源群集(以下称为群集* A )上从一个部门迁移到另一个部门,而副本群集(以下称为 B *)更新了该场景。交错事务中雇员以前部门的雇员计数。

数据 table 是使用以下 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;

这两个 table 的内容包括以下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      |
...
+-----+-------------+---------+

我们假设我们已经在使用异常 table,该异常 table 包括四个必需列(这些列用于此 table 的主键),用于操作类型和原因的可选列以及使用 SQL 语句创建的原始 table 的主键列如图所示:

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 *上,我们创建一个新部门,然后使用以下 SQL 语句将员工编号 999 移入该部门:

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 *上的事务将中止。

冲突作为READ_ROW操作(在群集* A *上)注册在异常 table 中(有关操作类型的说明,请参见冲突解决 exceptiontable),如下所示:

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 |
+-------+-------------+-------------------+

在读取操作中找到的所有现有行都被标记。这意味着,由相同冲突导致的多行可能会记录在异常 table 中,如检查群集* A 上的更新与从群集中同一 table 读取群集 B 上多行之间的冲突所产生的影响所示。同步 Transaction。在集群 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条件匹配的所有三行,并在 exceptiontable 中将它们标记出来,如下所示:

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 |
...
+-------+-------------+-------------------+

读取跟踪仅在现有行的基础上执行。基于给定条件的读取将仅与* found *的任何行发生冲突,而不与交错事务中插入的任何行发生冲突。这类似于在 NDB 群集的单个实例中执行排他行锁定的方式。