配置最后写入者冲突检测和解决方案

从 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)时,默认为参数CALLCALLSCALL用作命令类型@upd_cmd

    注释

    SQL Server 支持 SCALL@upd_cmd. 使用 时 SCALL,当事务将值更新为同一值时,它不被视为更改, SCALL 并且格式不提供未更新或修改的列的值。 有关 SCALL 调用格式的详细信息,请参阅 存储过程的调用语法

  • 可以将对等发布与可用性组中的最后一个编写器冲突检测和解决配合使用。 请参阅:

  • 可以看到冲突及其解决方法。

    • 在 SQL Server Management Studio 中,右键单击发布并选择“ 查看冲突”。

  • 插入和更新冲突是根据最后一个编写器解决的,但删除始终占上风。 例如,如果删除更新冲突且更新在以后完成,则删除仍会获胜。

  • 最后一个编写器冲突检测和解决是根据隐藏列 $sys_mw_cd_id确定的。 此列的数据类型为 datetime2

冲突检测比较

下表比较了如何检测和解决与传统的对等复制冲突,以及何时启用最后一个编写器冲突解决:

冲突类型 冲突详细信息 对等 最后一个编写器
Insert-Insert 每个参与对等复制的表中的所有行都是使用主键值唯一标识的。 当在多个节点上插入具有相同键值的行时,会发生插入-插入冲突。 如果传入行是赢家,则更新目标行。 在任一情况下,我们记录信息。 如果传入行是赢家,则更新目标行。 在任一情况下,我们记录信息。
Update-Update 在多个节点上更新同一行时发生。 如果传入行是赢家,则我们只修改更改后的列。 如果传入行是赢家,则我们修改目标处的所有列(如果 @upd_cmd 设置为 defaultCALL)。
Update-Insert 如果在一个节点上更新了某一行,但删除了同一行,然后在另一个节点上重新插入。 如果传入行是赢家,则我们只修改更改后的列。 当更新 peer1 行并且删除并重新插入 peer2同一行时,将发生这种情况。 发生同步时,将删除该 peer1 行,因为删除始终会赢,然后插入同一行,而该行将在以后更新时进行更新 peer2 。 这会导致非相交。
Insert- Update 如果删除了行,然后在一个节点上重新插入,并在另一个节点上更新了同一行,则会发生此事件。 如果传入行是赢家,则更新所有列。 当删除并重新插入 peer1 行并且在同一行上更新时 peer2,将发生这种情况。 同步发生时,该行将被删除 peer2 ,因为删除始终会赢,然后再次插入该行。 打开 peer1时,将跳过更新。
Delete-Insert

Insert-Delete
如果在一个节点上删除了一行,但删除了同一行,然后在另一个节点上重新插入。 我们目前认为这是 D-U 冲突,如果传入行是赢家,那么我们会从目标中删除该行。 当删除 peer1 行并且删除同一行时,将发生此情况, 同时重新插入该 peer2行。 同步发生时,将删除该 peer2 行,而该行插入到该行上 peer1。 之所以发生这种情况是因为我们不存储有关已删除行的信息,因此我们不知道该行是已删除的还是不存在于对等方上。 这会导致非相交。
Delete-Update 如果在一个节点上删除了某一行,但在另一个节点上更新了同一行,则会发生此情况。 我们目前认为这是 D-U 冲突,如果传入行是赢家,那么我们会从目标中删除该行。 这是 D-U 冲突。 随着删除始终获胜,传入删除是赢家,我们从目标中删除行。
Update-Delete 如果在一个节点上更新了某一行,但在另一个节点上删除了同一行,则会发生此情况。 在对等更新存储过程中,如果存在 U-D 冲突,我们将打印以下消息,并且不解决它。

An update-delete conflict was detected and unresolved. The row could not be updated since the row does not exist.
这是 U-D 冲突。 随着删除始终获胜,将跳过传入更新。
Delete-Delete 在多个节点上删除行时发生。 在对等删除存储过程中,如果有 D-D 冲突,则我们不会处理任何更改,只需记录它。 如果存在 D-D 冲突,则我们不会处理任何更改,只需记录它即可。

注释

在最后一个编写器冲突检测策略的当前实现中,当存在 insert-delete、delete-insert 或 update-delete 冲突时,删除始终会获胜。

例子

在第一个对等方上创建发布 (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)

以下脚本将在第二个对等方(节点 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