当使用涉及多个源(包括循环复制)的复制设置时,可能存在不同的源尝试在副本上使用不同的数据更新同一行。NDB 集群复制中的冲突解决提供了通过允许使用用户定义的解决列来解决此类冲突的方法,该列用于确定是否应在副本上应用给定源上的更新。
NDB 集群支持的一些冲突解决类型(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.ndb_replication
表中放置适当的条目来完成的(请参见 ndb_replication 表)。注意如果您正在复制包含非常大列(例如
TEXT
或BLOB
列)的表,--ndb-log-updated-only
对于减少二进制日志的大小以及避免因超过max_allowed_packet
而导致的可能复制失败也很有用。有关此问题的更多信息,请参见 第 19.5.1.21 节,“复制和 max_allowed_packet”。
在副本上,您必须确定要应用哪种类型的冲突解决(“最新时间戳获胜”、“相同时间戳获胜”、“主获胜”、“主获胜,完整事务” 或无)。这是使用
mysql.ndb_replication
系统表完成的,并适用于一个或多个特定表(请参见 ndb_replication 表)。NDB 集群还支持读取冲突检测,即检测一个集群中对给定行的读取与另一个集群中对同一行的更新或删除之间的冲突。这需要通过将副本上的
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 集群和辅助 NDB 集群上应用的复制纪元的相对顺序来工作,并且不使用时间戳。
我们可以将更新操作视为 “之前” 和 “之后” 映像——即应用更新之前和之后表的狀態。通常,在使用主键更新表时,“之前” 映像并不重要;但是,当我们需要在每次更新的基础上确定是否在副本上使用更新的值时,我们需要确保将这两个映像都写入源的二进制日志。这是通过使用 --ndb-log-update-as-write
选项来完成的,如本节后面所述。
冲突解决通常在可能发生冲突的服务器上启用。与日志记录方法选择一样,它通过 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 复制的冲突检测和解决的函数的详细信息。
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();
此函数可用于 “最大时间戳获胜” 冲突解决。这种类型的冲突解决确保在发生冲突的情况下,最近更新的行版本是持久存在的版本。
此函数对写入操作之间的冲突没有影响,除了具有与先前写入相同的键的写入操作总是被拒绝之外;只有在没有使用相同键的写入操作已经存在的情况下,它才会被接受并应用。您可以使用 NDB$MAX_INS()
来处理写入操作之间的冲突解决。
此函数使用源的 “之后” 映像中的列值。
NDB$MAX_DELETE_WIN()
这是 NDB$MAX()
的一种变体。由于删除操作没有时间戳,使用 NDB$MAX()
的删除操作实际上会被处理为 NDB$OLD
,但对于某些用例,这并不理想。对于 NDB$MAX_DELETE_WIN()
,如果来自源的给定行的添加或更新现有行的 “timestamp” 列值高于副本上的值,则会应用该值。但是,删除操作被视为始终具有较高值。以下伪代码说明了这一点
if ( (source_new_column_value > replica_current_column_value)
||
operation.type == "delete")
apply_update();
此函数可用于 “最大时间戳,删除获胜” 冲突解决。这种类型的冲突解决确保,在发生冲突的情况下,被删除或(以其他方式)最近更新的行版本是持续存在的版本。
与 NDB$MAX()
一样,源的 “after” 图像中的列值是此函数使用的值。
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()
是非对称的,它在一个双向复制配置(有时称为 “active-active” 复制)中的 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 Table)。在此情况下,主 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()
进行冲突检测时,当前仅支持双向 “active-active” 配置中的两个 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()
时,您应该记住,辅助节点会应用来自主节点的删除操作,删除新行,直到它被反映的操作恢复。理论上,辅助节点上的后续插入或更新与来自主节点的删除操作发生冲突,但在这种情况下,我们选择忽略此冲突并允许辅助节点 “获胜”,以防止集群之间发生分歧。换句话说,在删除操作之后,主节点不会检测到冲突,而是立即采用辅助节点的后续更改。因此,辅助节点的状态在过渡到最终(稳定)状态时可能会重新访问多个先前的已提交状态,其中一些状态可能是可见的。
您还应注意,将所有操作从辅助节点反映回主节点会增加主节点的 logbinary 日志的大小,以及对带宽、CPU 使用率和磁盘 I/O 的要求。
反映的操作在辅助节点上的应用取决于辅助节点上目标行的状态。可以通过检查 Ndb_conflict_reflected_op_prepare_count
和 Ndb_conflict_reflected_op_discard_count
状态变量,或者检查 Performance Schema ndb_replication_applier_status
表的 CONFLICT_REFLECTED_OP_PREPARE_COUNT
和 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$EPOCH()
找到的冲突行数来查看自从此副本上次从当前值重新启动以来 Ndb_conflict_fn_epoch
系统状态变量,或者通过检查 Performance Schema ndb_replication_applier_status
表的 CONFLICT_FN_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 以来,由于在给定 mysqld 上应用 ““相同时间戳获胜”” 冲突解决而导致未应用行的次数,由全局状态变量 Ndb_conflict_fn_old
给出。 除了递增 Ndb_conflict_fn_old
之外,未使用的行的主键还会插入到一个 异常表 中,在本节的其他地方对此进行了说明。
前面段落中引用的每个状态变量在 Performance Schema ndb_replication_applier_status
表中都有一个等效的列。 有关更多信息,请参见此表的说明。 另请参见 第 25.4.3.9.3 节,“NDB Cluster 状态变量”。
以下示例假设您已经按照 第 25.7.5 节,“为 NDB Cluster 准备复制” 和 第 25.7.6 节,“启动 NDB Cluster 复制(单个复制通道)” 中的说明,已经设置了可用的 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 表。将 0 插入到
server_id
列中表示所有访问此表的 SQL 节点都应使用冲突解决。 如果您只想在特定 mysqld 上使用冲突解决,请使用实际的服务器 ID。将
NULL
插入到binlog_type
列中与插入 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 上,我们创建一个新部门,然后使用以下 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 上的事务将被中止。
冲突在异常表(在集群 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)
如前所述,源上插入的其他行都没有被副本拒绝,只有那些具有小于副本上冲突行中时戳值的行的行。