从 SQL Server 2019 (15.x) CU13 开始,您可以配置点对点复制,使其通过允许最近的插入或更新操作胜出冲突来自动解决冲突。 如果任一写入操作删除了该行,SQL Server 会允许删除操作在冲突中胜出。 这种方法称为“最后写入者胜出”。
使用存储过程配置“最后写入者胜出”规则。 使用“最后写入者”时,请勿使用“点对点拓扑向导”来添加或删除节点。
重要配置注意事项
注释
升级到 SQL Server 2019 (15.x) CU13 或更高版本后,当您将发布配置为使用“最后写入者获胜”冲突解决策略时,该发布中会包含额外的元数据。 如果以后卸载/降级到低于 SQL Server 2019(15.x) CU13 的版本,此额外元数据会导致问题。 在降级之前,应删除任何此类发布,然后在较低版本上重新创建它们。
当您配置具有自动冲突发现和解决功能的点对点复制,并采用“最后写入者获胜”规则时,请包含以下配置和设置:
使用以下参数创建该发布内容,
设置
@p2p_conflictdetection_policy = 'lastwriter'以指定“最后写入者获胜”。 请参阅sp_addpublication(Transact-SQL)。 此参数在 SQL Server 2019 (15.x) CU13 中引入。 默认值originatorid基于发起方 ID 解决冲突,与 SQL Server 2019 (15.x) CU13 之前的冲突解决相同。设置为
@p2p_continue_onconflict = 'true'允许分发代理解决冲突。
添加文章(
sp_addarticle)时,请确认更新命令的命令类型行为(@upd_cmd)。 选项包括:-
CALL(默认值) SCALL
请参阅 sp_addarticle中的详细信息。
-
在具有“最后写入者”冲突检测策略的发布中添加文章 (
sp_addarticle) 时,请将CALL或SCALL作为@upd_cmd参数的命令类型,默认值为CALL。注释
SQL Server 支持将
SCALL用作@upd_cmd的值。 使用SCALL时,如果事务将某个值更新为相同的值,则不会被视为变更,并且SCALL格式不会提供未更新或未修改列的值。 有关 SCALL 调用格式的详细信息,请参阅 存储过程的调用语法。您可以在可用性组中使用具有“最后写入者”冲突检测和解决功能的点对点发布。 请参阅:
可以看到冲突及其解决方法。
- 在 SQL Server Management Studio 中,右键单击发布并选择“ 查看冲突”。
或
- 在发布数据库上查询
conflict_schemaname_tablename。 例如,conflict_dbo_tab1。 请参阅 conflict_<schema>_<table> (Transact-SQL)。
插入和更新冲突根据“最后写入者”原则进行解决,但删除操作始终优先。 例如,如果发生删除-更新冲突,并且更新发生得更晚,删除仍然胜出。
最后写入者冲突的检测和解决基于隐藏列
$sys_mw_cd_id确定。 此列的数据类型为 datetime2。
冲突检测比较
下表比较了在启用最后写入者冲突解决时,与传统点对点复制相比,冲突的检测和解决方式:
| 冲突类型 | 冲突详细信息 | 点对点 | 最后写入者 |
|---|---|---|---|
| 插入-插入 | 参与对等复制的每个表中的所有行都通过主键值进行唯一标识。 当在多个节点上插入具有相同键值的行时,会发生插入-插入冲突。 | 如果传入的行是胜出方,则更新目标行。 在任一情况下,我们记录信息。 | 如果传入的行是胜出方,则更新目标行。 在任一情况下,我们记录信息。 |
| 更新-更新 | 在多个节点上更新同一行时发生。 | 如果传入的行是胜出方,则我们仅修改已更改的列。 | 如果传入的行胜出,则我们会修改目标位置中的所有列(如果 @upd_cmd 设置为 default – CALL)。 |
| 更新-插入 | 如果某一行在一个节点上被更新,但同一行在另一个节点上被删除后又重新插入,则会发生此情况。 | 如果传入的行是胜出方,则我们仅修改已更改的列。 | 当某一行在 peer1 上被更新,而同一行在 peer2 上被删除并重新插入时,就会发生这种情况。 发生同步时,peer1 上的该行会被删除,因为删除操作始终优先,随后又插入同一行;而在 peer2 上,该行会被更新,因为更新发生在更晚的时间。 这会导致数据无法收敛。 |
| 插入-更新 | 如果删除了行,然后在一个节点上重新插入,并在另一个节点上更新了同一行,则会发生此事件。 | 如果传入的行是胜出方,则我们更新所有列。 | 当在 peer1 上删除并重新插入某一行,同时在 peer2 上更新同一行时,就会发生这种情况。 发生同步时,该行会在 peer2 上被删除,因为删除始终优先,随后会再次插入。 在 peer1 上,将跳过更新。 |
| 删除-插入 插入-删除 |
如果某个节点上删除了一行,而在另一个节点上,同一行先被删除,然后又被重新插入,就会发生这种情况。 | 我们目前将此视为 D-U 冲突,如果传入的行是胜出方,则从目标端删除该行。 | 当某一行在 peer1 上被删除,而同一行在 peer2 上被删除后又重新插入时,就会发生这种情况。 当同步发生时,peer2 上的该行被删除,而在 peer1 上插入该行。 之所以发生这种情况是因为我们不存储有关已删除行的信息,因此我们不知道该行是已删除的还是不存在于对等方上。 这会导致数据无法收敛。 |
| 删除-更新 | 如果在一个节点上删除了某一行,但在另一个节点上更新了同一行,则会发生此情况。 | 我们目前将此视为 D-U 冲突,如果传入的行是胜出方,则从目标端删除该行。 | 这是 D-U 冲突。 由于删除操作始终优先,传入的删除操作即为胜出方,因此我们会从目标端删除该行。 |
| 更新-删除 | 如果在一个节点上更新了某一行,但在另一个节点上删除了同一行,则会发生此情况。 | 在点对点更新存储过程中,若发生 U-D 冲突,则会输出以下消息并放弃解决该冲突。An update-delete conflict was detected and unresolved. The row could not be updated since the row does not exist. |
这是一起 U-D 冲突。 由于删除操作始终具有优先权,因此将跳过传入的更新。 |
| 删除-删除 | 当某一行在多个节点上被删除时发生。 | 在点对点删除存储过程中,若发生 D-D 冲突,则不处理任何更改,仅进行记录。 | 如果存在 D-D 冲突,则我们不会处理任何更改,只需记录它即可。 |
注释
在当前“最后写入者”冲突检测策略的实现中,当发生插入-删除、删除-插入或更新-删除冲突时,删除操作始终优先。
例子
在第一个对等节点(Node1)上创建发布内容
在此示例中,脚本:
- 发布名为
MWPubDB的数据库。 - 将该出版物命名为
PublMW。 - 将冲突检测和解决策略配置为“最后写入者胜出”策略:
, @p2p_continue_onconflict= 'true', @p2p_conflictdetection_policy = 'lastwriter'
USE [MWPubDB];
EXECUTE sp_replicationdboption
@dbname = N'MWPubDB',
@optname = N'publish',
@value = N'true';
GO
-- Adding the transactional publication
USE [MWPubDB];
EXECUTE sp_addpublication
@publication = N'PublMW',
@description = N'Peer-to-Peer publication of database ''MWPubDB'' from Publisher ''Node1''.',
@sync_method = N'native',
@retention = 0,
@allow_push = N'true',
@allow_pull = N'true',
@allow_anonymous = N'false',
@enabled_for_internet = N'false',
@snapshot_in_defaultfolder = N'true',
@compress_snapshot = N'false',
@ftp_port = 21,
@allow_subscription_copy = N'false',
@add_to_active_directory = N'false',
@repl_freq = N'continuous',
@status = N'active',
@independent_agent = N'true',
@immediate_sync = N'true',
@allow_sync_tran = N'false',
@allow_queued_tran = N'false',
@allow_dts = N'false',
@replicate_ddl = 1,
@allow_initialize_from_backup = N'true',
@enabled_for_p2p = N'true',
@enabled_for_het_sub = N'false',
@p2p_conflictdetection = N'true',
@p2p_originator_id = 100,
@p2p_continue_onconflict = 'true',
@p2p_conflictdetection_policy = 'lastwriter';
GO
USE [MWPubDB];
EXECUTE sp_addarticle
@publication = N'PublMW',
@article = N'tab1',
@source_owner = N'dbo',
@source_object = N'tab1',
@type = N'logbased',
@description = NULL,
@creation_script = NULL,
@pre_creation_cmd = N'drop',
@schema_option = 0x0000000008035DDB,
@identityrangemanagementoption = N'manual',
@destination_table = N'tab1',
@destination_owner = N'dbo',
@status = 16,
@vertical_partition = N'false',
@ins_cmd = N'CALL sp_MSins_dbotab1',
@del_cmd = N'CALL sp_MSdel_dbotab1',
@upd_cmd = N'CALL sp_MSupd_dbotab1';
GO
在第二个对等节点(Node2)上创建发布
以下脚本在第二个对等节点(Node 2)上创建发布。
USE [MWPubDB];
EXECUTE sp_replicationdboption
@dbname = N'MWPubDB',
@optname = N'publish',
@value = N'true';
GO
-- Adding the transactional publication
USE [MWPubDB];
EXECUTE sp_addpublication
@publication = N'PublMW',
@description = N'Peer-to-Peer publication of database ''MWPubDB'' from Publisher ''Node2''.',
@sync_method = N'native',
@retention = 0,
@allow_push = N'true',
@allow_pull = N'true',
@allow_anonymous = N'false',
@enabled_for_internet = N'false',
@snapshot_in_defaultfolder = N'true',
@compress_snapshot = N'false',
@ftp_port = 21,
@allow_subscription_copy = N'false',
@add_to_active_directory = N'false',
@repl_freq = N'continuous',
@status = N'active',
@independent_agent = N'true',
@immediate_sync = N'true',
@allow_sync_tran = N'false',
@allow_queued_tran = N'false',
@allow_dts = N'false',
@replicate_ddl = 1,
@allow_initialize_from_backup = N'true',
@enabled_for_p2p = N'true',
@enabled_for_het_sub = N'false',
@p2p_conflictdetection = N'true',
@p2p_originator_id = 1,
@p2p_continue_onconflict = 'true',
@p2p_conflictdetection_policy = 'lastwriter';
GO
USE [MWPubDB];
EXECUTE sp_addarticle
@publication = N'PublMW',
@article = N'tab1',
@source_owner = N'dbo',
@source_object = N'tab1',
@type = N'logbased',
@description = NULL,
@creation_script = NULL,
@pre_creation_cmd = N'drop',
@schema_option = 0x0000000008035DDB,
@identityrangemanagementoption = N'manual',
@destination_table = N'tab1',
@destination_owner = N'dbo',
@status = 16,
@vertical_partition = N'false',
@ins_cmd = N'CALL sp_MSins_dbotab1',
@del_cmd = N'CALL sp_MSdel_dbotab1',
@upd_cmd = N'CALL sp_MSupd_dbotab1';
GO
创建从 Node1 到 Node2 的订阅
USE [MWPubDB];
EXECUTE sp_addsubscription
@publication = N'PublMW',
@subscriber = N'Node2',
@destination_db = N'MWPubDB',
@subscription_type = N'Push',
@sync_type = N'replication support only',
@article = N'all',
@update_mode = N'read only',
@subscriber_type = 0;
GO
EXECUTE sp_addpushsubscription_agent
@publication = N'PublMW',
@subscriber = N'Node2',
@subscriber_db = N'MWPubDB',
@job_login = NULL,
@job_password = NULL,
@subscriber_security_mode = 1,
@frequency_type = 64,
@frequency_interval = 1,
@frequency_relative_interval = 1,
@frequency_recurrence_factor = 0,
@frequency_subday = 4,
@frequency_subday_interval = 5,
@active_start_time_of_day = 0,
@active_end_time_of_day = 235959,
@active_start_date = 0,
@active_end_date = 0,
@dts_package_location = N'Distributor';
GO
创建从 Node2 到 Node1 的订阅
USE [MWPubDB];
EXECUTE sp_addsubscription
@publication = N'PublMW',
@subscriber = N'Node1',
@destination_db = N'MWPubDB',
@subscription_type = N'Push',
@sync_type = N'replication support only',
@article = N'all',
@update_mode = N'read only',
@subscriber_type = 0;
GO
EXECUTE sp_addpushsubscription_agent
@publication = N'PublMW',
@subscriber = N'Node1',
@subscriber_db = N'MWPubDB',
@job_login = NULL,
@job_password = NULL,
@subscriber_security_mode = 1,
@frequency_type = 64,
@frequency_interval = 1,
@frequency_relative_interval = 1,
@frequency_recurrence_factor = 0,
@frequency_subday = 4,
@frequency_subday_interval = 5,
@active_start_time_of_day = 0,
@active_end_time_of_day = 235959,
@active_start_date = 0,
@active_end_date = 0,
@dts_package_location = N'Distributor';
GO