25.7.12 NDB 集群复制冲突解决
在使用多个源的复制设置(包括循环复制)时,可能会发生不同源尝试更新副本中相同行的不同数据的情况。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
列中使用模式匹配的信息,请参阅使用通配符进行匹配。
您还应该记住,应用程序负责确保解决列正确地被填充有相关值,以便当确定是否应用更新时,解析函数可以做出适当的选择。
在解决冲突之前,必须在源和副本上进行准备。这些任务在以下列表中被描述:
-
在写入二进制日志的源服务器上,您必须确定哪些列被发送(所有列或仅更新过的列)。这对于MySQL Server作为一个整体是通过应用mysqld启动选项
--ndb-log-updated-only
(在本节稍后描述)或通过在一个或多个特定表中放置适当条目来完成,条目位于mysql.ndb_replication
表中(参见ndb_ replication 表)。Note如果您正在复制包含非常大列的表(例如
TEXT
或BLOB
列),--ndb-log-updated-only
也可以有助于减少二进制日志的大小并避免由于max_allowed_packet
超限而导致的复制失败。有关此问题的更多信息,请参阅第19.5.1.20节,“Replication and max_allowed_packet”。
-
在复制副本上,您必须确定如何应用冲突解决方案(“最新时间戳获胜”、“同一时间戳获胜”、“主服务器获胜”、“主服务器获胜,完整事务”或无)。这使用了
mysql.ndb_replication
系统表,并适用于一个或多个特定表(请参阅NDB复制表)。 -
NDB集群还支持读取冲突检测,即在一个集群中读取给定行的同时,另一个集群中的更新或删除操作。这个需要独占的读锁,由设置
ndb_log_exclusive_reads
等于1在副本上来实现。这将导致所有冲突读取的行被记录到异常表中。有关更多信息,请参阅读取冲突检测和解决。 -
使用
NDB$MAX_INS()
或NDB$MAX_DEL_WIN_INS()
时,NDB
可以将WRITE_ROW
事件应用于幂等性,即将这样的事件映射到插入操作,如果incoming行不存在,或者更新操作如果它存在。使用任何冲突解决函数(除了
NDB$MAX_INS()
或NDB$MAX_DEL_WIN_INS()
)时,如果行已经存在,所有写入都会被拒绝。
当使用基于时间戳的冲突解决函数 NDB$OLD()
、NDB$MAX()
、NDB$MAX_DELETE_WIN()
、NDB$MAX_INS()
和 NDB$MAX_DEL_WIN_INS()
时,我们经常将用于确定更新的列称为一个 “时间戳” 列。然而,这个列的数据类型从未是 “时间戳” 列应该是 UNSIGNED
并且 NOT NULL
。
稍后在本节中讨论的 NDB$EPOCH()
和 NDB$EPOCH_TRANS()
函数通过比较主和从 NDB 集群上应用的相对顺序的复制epochs,并不使用时间戳。
我们可以看到更新操作在“before”和“after”图像中—that is, 表的状态在更新应用之前和之后。通常,当使用主键更新表时,“before”图像是不太感兴趣的;然而,当我们需要根据每次更新决定是否在复制品上使用更新后的值时,我们需要确保两个图像都写入源的二进制日志。这是通过为mysqld设置--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_类型
值时不正确地工作。
本节提供了有关用于NDB复制冲突检测和解决的函数的详细信息。
如果源和副本中column_名称
的值相同,则更新将被应用;否则,更新不会在副本上应用,并且异常将写入日志。这由以下伪代码所示:
if (source_old_column_value == replica_current_column_value)
apply_update();
else
log_exception();
这个函数可以用于“相同值获胜”冲突解决。这种类型的冲突解决确保了不会在副本上从错误源应用更新。
这个函数使用源的“before”图像中的列值。
对于更新或删除操作,如果源中给定行的“timestamp”列值高于副本上的值,则它将被应用;否则,它不会在副本上应用。这由以下伪代码所示:
if (source_new_column_value > replica_current_column_value)
apply_update();
这个函数可以用于“最新的时间戳获胜”冲突解决。这类冲突解决确保了,在发生冲突时,行的最晚更新版本将是持久的版本。
这个函数对写操作之间的冲突没有影响,只是在有相同主键的写操作之前已经存在时,它的写操作总是被拒绝;它只在没有使用相同主键的其他写操作已存在时才被接受并应用。你可以使用NDB$MAX_INS()
来处理写操作之间的冲突解决。
这个函数使用源的“after”图像中的列值。
这是对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()
相同)。 -
否则,应用 “最大的时间戳获胜者” 冲突解决方案,如下所示:
-
如果incoming写入的时间戳大于冲突写入的时间戳,应用incoming操作。
-
如果incoming写入的时间戳不是大于的,拒绝incoming写入操作。
-
在处理插入操作时,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_DELETE_WIN()
一样“删除优先”(delete wins)的解决方案。写入冲突由 NDB$MAX_DEL_WIN_INS()
如下所示进行处理:
-
如果没有冲突的写操作,应用这个操作(这与
NDB$MAX_DELETE_WIN()
相同)。 -
否则,按照“最大时间戳优先”(greatest timestamp wins)的冲突解决策略进行,如下所示:
-
如果incoming写操作的时间戳大于冲突写操作的时间戳,则应用incoming操作。
-
如果incoming写操作的时间戳不大于,则拒绝incoming写操作。
-
由 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();
删除操作使用“删除总是优先”(delete always wins)的策略(与 NDB$MAX_DELETE_WIN()
相同);无论时间戳值如何,始终应用一个 DELETE
,如伪代码所示:
if (operation.type == "delete")
apply_delete();
在更新和删除操作之间的冲突时,这个函数与 NDB$MAX_DELETE_WIN()
行为一致。
NDB$EPOCH()
函数跟踪在复制集群的副本上相对于来自副本的更改应用顺序的重复周期。这一相对顺序用于确定是否有来自副本的更改与在副本本地发生的更改是并行的,因此可能存在冲突。
大多数 NDB$EPOCH()
描述中的内容也适用于 NDB$EPOCH_TRANS()
。任何异常都在文本中注明。
NDB$EPOCH()
是不对称的,在双向复制配置中操作(有时称为 “主动-主动” 复制)。这里提到的它所在的集群被称为主要集群,而另一组被称为次要集群。主要集群上的副本负责检测和处理冲突,而次要集群中的副本不参与任何冲突检测或处理。
当主要集群上的副本检测到冲突时,它将事件注入其自己的二进制日志以进行补偿;这确保了最终次要 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位的值,因此除非对TimeBetweenGlobalCheckpoints
、TimeBetweenEpochs
或两者都设置了其他值,否则默认值(6)应该足够。一个过小的值可能导致假阳性,而一个过大的值可能会导致数据库中过多的浪费空间。
同时NDB$EPOCH()
和NDB$EPOCH_TRANS()
都会将冲突行插入到相关异常表中,前提是这些表已经根据本节其他地方描述的异常表模式规则定义了。您必须在使用它的数据表之前创建任何异常表。
与本节讨论的其他冲突检测函数一样,NDB$EPOCH()
和NDB$EPOCH_TRANS()
通过在mysql. ndb_replication
表中包含相关条目来激活(见ndb_ replication Table)。在这种情况下,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()
或NDB$EPOCH_TRANS()
的表,其中包含
NDB$EPOCH_TRANS()
扩展了 NDB$EPOCH()
函数。冲突在同样的方式中被检测并处理,使用“主键优先” 规则(参见NDB$EPOCH()),但有额外的条件:在冲突发生的同一事务中更新的任何其他行也被认为是冲突的。换句话说,NDB$EPOCH()
在次级上重新对线性冲突的单个行进行对齐,而 NDB$EPOCH_TRANS()
在次级上重新对冲突的事务进行对齐。
此外,任何可以检测到的依赖于冲突事务的事务也被认为是冲突的,这些依赖关系由次级集群的二进制日志内容确定。由于二进制日志仅包含数据修改操作(插入、更新和删除),只有重叠的数据修改才用于确定事务之间的依赖性。
NDB$EPOCH_TRANS()
受同样的条件和限制约束,另外还要求次级在其二进制日志中记录所有事务ID,使用--ndb-log-transaction-id
设置为 ON
。这增加了额外的开销(每行至多13字节)。
请参阅NDB$EPOCH()。
NDB$EPOCH2()
函数与 NDB$EPOCH()
类似,但 NDB$EPOCH2()
在具有双向复制拓扑的删除-删除场景中提供了处理。这种情况下,通过将 ndb_ conflict_role
系统变量设置为适当值(通常是 PRIMARY
和 SECONDARY
)来分配主和从角色给两个源。在这样做时,修改由从节点进行的将被反射到主节点,然后条件地在从节点上应用。
NDB$EPOCH2_TRANS()
扩展了 NDB$EPOCH2()
函数。冲突检测和处理与之相同,分配给复制集群的主和从角色,但有额外条件,即在发生冲突的事务中更新的任何其他行也被认为是冲突的一部分。这就是说 NDB$EPOCH2()
在次节点上重新对齐单个冲突行,而 NDB$EPOCH_ TRANS()
重新对齐冲突的事务。
在 NDB$EPOCH()
和 NDB$EPOCH_TRANS()
中,使用每行的元数据以及每个最后修改周期的元数据来确定主服务器是否将从副本服务器接收到的复制行变更与本地提交的变更是并发发生的;并发变更被认为是冲突的,并且在异常表更新和副本服务器的重新对齐之后会引起异常。问题出现在主服务器上删除了一个行,所以没有可用于确定是否有任何复制操作冲突的最后修改周期可用,这意味着不会检测到冲突的删除操作。这可能导致分歧,例如在一个集群中删除一行,而在另一个集群中删除并插入;这就是为什么当使用 NDB$EPOCH()
和 NDB$EPOCH_TRANS()
时,删除操作只能路由到一个集群。
NDB$EPOCH2()
避免了上述问题——在主服务器上存储关于已删除行的信息——通过忽略任何删除-删除冲突,并避免任何潜在的分歧结果。这是通过反射从副本成功应用并复制到副本的操作来实现的。在返回副本时,它可以用来重新应用由主服务器发起的操作对副本进行操作。
当使用 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
)。
事件将被应用,如果且仅如果满足以下两个条件之一:
-
该行的存在性,即它是否存在,与事件类型相符。对于删除和更新操作,该行必须已经存在。对于插入操作,该行必须不存在。
-
该行最后由主节点修改。可能是通过执行反射操作来完成的修改。
如果这两个条件都不满足,secondary就会丢弃反射操作。
要使用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集群支持三个预定义的可选列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_不再存在
可能会作为 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
引用更新和删除操作中的旧值—that is, 包含 colname
$OLDDELETE_ROW
事件的操作。
可以用于引用插入和更新操作中的新值—in other words, 使用 colname
$NEWWRITE_ROW
事件、UPDATE_ROW
事件或两种类型的事件的操作。在发生冲突的操作中,没有为给定参考列提供值(如果该参考列不是主键),异常表中的行将包含 NULL
,或者为该列定义了默认值。
当数据表设置为复制时,mysql.ndb_replication
表被读取,因此要复制的表对应的行必须在要复制的表创建之前插入到 mysql.ndb_replication
中。
有几个状态变量可以用来监控冲突检测。你可以通过 NDB$EPOCH()
自从这个副本最后重启以来找到有多少行发生了冲突,从当前 Ndb_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 以来,由于 “相同时间戳获胜” 冲突解决策略导致的行未被应用的次数由全局状态变量 Ndb_conflict_fn_old
给出。此外,除了增加 Ndb_conflict_fn_old
之外,还将该行的主键插入一个 异常表,如本节其他地方所述。
以下示例假设您已经设置了一个工作的NDB集群复制环境,正如第25.7.5节“为复制准备NDB集群”和
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)');
NoteIf the
ndb_replication
table does not already exist, you must create it. See ndb_replication Table.将一个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;
Note由于我们在表定义中至少包含了
NDB$OP_TYPE
、NDB$CFT_CAUSE
或NDB$ORIG_TRANSID
中的列之一,需要为四个必需列添加NDB$
前缀。 -
创建如前所示的表
test.t2
。
对于每个表,必须遵循这些步骤,以便使用 NDB$OLD()
进行冲突解决。对于每个这样的表,都必须在 mysql.ndb_replication
中有一个相应的行,并且该数据库中必须存在与被复制表相同的异常表。
读取冲突检测和解决. NDB 集群还支持跟踪读操作,这使得在循环复制设置中可以管理两个集群之间给定行的读取冲突和更新或删除。这个例子使用了 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
条件匹配的所有三个行都会被读取,并因此在异常表中被标记,如下所示:
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集群实例中执行的排他行锁定类似。
插入冲突检测和解决方案示例。 以下是使用插入冲突检测函数的示例。我们假设我们正在复制两个表t1
和t2
,并且我们希望使用NDB$MAX_INS()
对t1
进行插入冲突检测,并对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_全用更新
(7
),这意味着总是记录完整的行。请参阅NDB 表,了解其他可能的值。
您还可以为每个 NDB
表创建一个异常表,该表记录了由冲突解决函数拒绝的所有行。对于给定表的复制冲突检测,使用以下两个 SQL 语句可以创建异常表:
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)
正如我们之前看到的,只有那些与复制服务器上冲突行时间戳值小于的行由源端插入而被复制服务器拒绝。