在 SQL Server 复制拓扑中,发布端与订阅端出现数据不一致,通常不是单一故障导致的。事务复制依赖日志读取代理从发布数据库读取事务日志,再由分发代理传递到订阅端。只要其中一个环节发生跳过、延迟或错误,就会表现为同一张表两边的行数或字段值不同。更隐蔽的情况是复制命令已经到达订阅端,但因为外键、触发器或权限问题执行失败,而这些错误又被配置为跳过。因此,解决不一致不能只盯着复制监视器,还要结合行级对比和验证工具,先确定差异范围,再决定是补数据还是重新初始化。

一、先区分复制延迟与真实数据差异
复制监视器里显示代理在运行,并不代表订阅端数据一定已经追平。延迟可能是短期的,例如一次大批量导入产生了大量日志,分发代理需要时间消化;也可能是长期的,比如订阅端磁盘压力大、网络带宽被占满或者分发数据库空间不足。判断延迟大小可以执行 sp_replcounters,它会返回日志读取延迟和分发延迟的秒数。如果延迟持续增长,说明复制吞吐已经跟不上发布端的变更速度,这时候订阅端看起来不一致,其实只是落后,通常不需要重新初始化,优化代理参数或清理阻塞即可。
真正需要干预的是真实数据差异。典型信号包括:分发代理报错后停止、错误被配置为跳过、订阅端手工修改过数据、或者复制命令在订阅端执行失败但未重试。此时即使等待更长时间,差异也不会自动消失。可以通过 sp_browsereplcmds 查看分发数据库中尚未投递或投递失败的命令,也可以查询 MSrepl_errors 表获取错误记录。先确认差异是延迟还是永久缺失,能避免盲目执行重初始化造成订阅端业务中断。
二、使用 tablediff 与复制验证定位差异范围
tablediff 是 SQL Server 自带的命令行工具,用于比较发布端和订阅端同一张表的数据。它以主键或指定列为基准逐行比对,能够生成目标端缺失行和差异行的修复脚本。例如要比较两个服务器上的 Orders 表,可以执行以下命令。参数 -f 指定输出修复脚本的路径,-et 指定差异明细表名称。
tablediff -sourceserver "PubServer" -sourcedatabase "PubDB" -sourcetable "dbo.Orders" -destinationserver "SubServer" -destinationdatabase "SubDB" -destinationtable "dbo.Orders" -f C:\Temp\orders_diff.sql -et Diffs
如果只想聚焦关键列,可以加上 -c 参数,例如 -c OrderID,CustomerID,TotalAmount。tablediff 还能配合 -rowcounts 仅比较行数,适合快速判断是否存在大范围缺失。生成的修复脚本默认是 INSERT 和 UPDATE 语句,可以在订阅端执行,但执行前必须确认订阅端没有并发复制写入,否则可能产生主键冲突或覆盖新数据。
除了行级对比,复制本身也提供验证机制。sp_publication_validation 会在发布端计算文章的行数和校验和,sp_subscription_validation 则在订阅端执行同样的计算。两者结果不一致说明存在数据漂移。验证结果可以通过复制监视器或分发库中的 MSmerge_history 查看。定期运行验证任务比手工执行 tablediff 更适合纳入日常监控体系。
EXEC sp_publication_validation @publication = N'pub_Orders', @rowcount_only = 1; EXEC sp_subscription_validation @publication = N'pub_Orders';
三、选择修复策略:定向补数、单表重初始化与全量重初始化
差异范围较小且业务允许短暂停写时,可以采用定向补数方案。tablediff 生成的修复脚本只包含缺失行和变化行,对订阅端影响最小。执行脚本前建议暂停相关文章的分发代理,或者在业务低峰窗口完成,避免复制代理与手工补数互相干扰。补数完成后启动代理,如果代理继续从最后投递点开始,可能会重复应用同一批命令,因此需要确认主键和唯一约束能够容忍重复尝试,或者在补数后标记已同步点。
差异集中在某一张或某几张表时,可以按文章重新初始化,而不是全库重建。使用 sp_reinitsubscription 可以只对指定文章重新生成快照并推送到订阅端。这种方式比全量重初始化速度快,对业务影响更可控。下面示例仅重新初始化 Orders 表。
EXEC sp_reinitsubscription
@publication = N'pub_Orders',
@article = N'Orders',
@subscriber = N'SubServer',
@destination_db = N'SubDB';
如果差异涉及多张表、架构已经不一致或者订阅端数据被大面积破坏,就需要全量重新初始化。全量重初始化会重新创建发布快照并推送,订阅端相关文章会被覆盖。执行前必须备份订阅端上任何仍有价值的本地数据,并确认业务可以接受快照同步期间的访问中断。全量初始化应避开高峰,必要时可以拆分为多个文章分批执行,降低对日志和网络的压力。
有些生产环境为了不让复制中断,会配置跳过错误参数 skipErrors。例如跳过主键冲突错误 2601。下面的写法可以临时让代理忽略这类错误,但本质上只是掩盖问题。跳过错误后订阅端会永久缺少相应事务,必须通过后续对账和补数来修正。因此跳过错误只能作为应急手段,不能作为长期策略。
EXEC sp_setsubscription_properties
@publisher = N'PubServer',
@publisher_db = N'PubDB',
@publication = N'pub_Orders',
@property = N'skipErrors',
@value = 2601;
四、通过约束、监控和变更管理预防再次不一致
防止复制不一致的关键是减少订阅端被意外修改的机会。订阅数据库应当设置为只读,或者通过权限控制禁止应用账号直接写入复制表。很多不一致并不是复制链路本身的问题,而是有人绕过复制直接改订阅端造成的。对于报表或查询场景,应使用只读账号,并关闭订阅端应用对基础表的写权限,从源头消除双向冲突。订阅端的外键约束和触发器也常常导致复制命令失败,因为发布端已经保证过的约束在订阅端会再次检查。如果确有需要,可以在订阅端使用 NOT FOR REPLICATION 选项让触发器或约束对复制进程放行。
架构变更也必须纳入复制管理。发布端的表结构发生变化时,如果只在发布端执行了 ALTER TABLE,订阅端可能没有对应列或约束,后续复制命令就会失败。应优先使用复制架构更改传播机制,例如执行 sp_repladdcolumn,或者配置发布属性使 DDL 自动复制。不要手工只改一端,否则即使暂时没有报错,也会在后续批量操作时暴露问题。日常应定期执行行计数校验,特别是在每次架构升级或大批量导入之后立即验证关键表。
监控层面建议在复制监视器中设置告警,针对代理停止、未同步订阅、过期订阅等事件触发通知。同时可以建立 SQL 作业,每周对核心表运行一次 tablediff 或 sp_publication_validation,将差异结果写入审计表。对于大批量写入操作,尽量避免单个事务包含数百万行,分批提交既能减少日志读取压力,也能缩短复制延迟。网络、存储和分发库空间也是稳定性因素,提前扩容并监控磁盘占用可以避免复制因空间不足而失败。通过约束、监控和规范变更,复制不一致才能从被动修复转变为主动预防。
SQL Server复制数据一致性发布订阅修改时间:2026-08-27 12:37:55