配置上次编写器冲突检测和解决方案

从 SQL Server 2019 (15.x) CU 13 开始,可将对等复制配置为允许最近的插入或更新操作在冲突时胜出,以此自动解决冲突。 如果任一写入操作删除了行,则 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) CU 13 中引入。 默认值 originatorid 根据发起方 ID 解决冲突,与 SQL Server 2019 (15.x) CU 13 之前的冲突解决方案相同。
    • 设置 @p2p_continue_onconflict = 'true',允许分发代理解决冲突。
  • 添加项目 (sp_addarticle) 时,请确认更新命令 (@upd_cmd) 的命令类型行为。 选项包括:

    • CALL(默认值)
    • SCALL

    请参阅 sp_addarticle (Transact-SQL) 中的详细信息。

  • 使用最后写入者冲突检测策略添加项目 (sp_addarticle) 时,使用 CALLSCALL 或作为 @upd_cmd 参数的命令类型,默认为 CALL

    注意

    SQL Server 支持将 SCALL 用于 @upd_cmd。 使用 SCALL 时,如果某项事务将值更新为相同的值,该操作不会被视为更改,并且 SCALL 格式不会对未更新或修改的列提供该值。 请查看以下内容,详细了解 SCALL 调用格式 - 存储过程的调用语法

  • 可以将对等发布与可用性组中最后写入者冲突检测和解决方案一起使用。 请参阅:

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

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

  • 插入和更新操作冲突根据最后写入者进行解决,但删除操作始终优先。 例如,如果发生删除-更新冲突,并且更新操作是后完成的,删除操作仍然胜出。

  • 最后写入者冲突检测和解决方案是根据隐藏列 $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]
EXEC sp_replicationdboption @dbname = N'MWPubDB'
  , @optname = N'publish'
  , @value = N'true'
GO
-- Adding the transactional publication
USE [MWPubDB]
EXEC 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]
EXEC 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) 上创建发布

以下脚本在第二个对等节点 (Node2) 上创建发布。

USE [MWPubDB]
EXEC sp_replicationdboption @dbname = N'MWPubDB'
 , @optname = N'publish'
 , @value = N'true'
GO
-- Adding the transactional publication
USE [MWPubDB]
EXEC 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]
EXEC 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]
EXEC 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
EXEC 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]
EXEC 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
EXEC 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