我一直觉得,数据库同步这活,干好了是细水长流,干不好就是定时炸弹。前阵子帮一个项目组把核心库的几张业务表实时同步到报表服务器,用的方案就是 SQL Server 事务发布——也就是我们常说的“事务复制”(Transaction Replication)。这套方案在 SQL Server 生态里非常老牌,成熟稳定,而且核心机制是基于事务日志读取,对源库的额外压力非常小。接下来我会从原理、选型、配置、排错、维护这五个维度,把事务发布实现数据同步功能的完整链路拆开讲一遍。
这篇文章比较长,但每一段都有能直接落地的成分。如果你第一次接触事务发布,按着实操章节走一遍,基本能把一套发布从零搭起来;如果你已经在生产环境跑过一段时间的复制,后半段的排查记录和调优经验,说不定能帮你省下不少半夜被拉起来值班的精力。
1. 先想清楚:你的同步需求到底属于哪一类
1.1 我又不傻,为什么要专门搭一套同步
很多业务刚开始只有一套数据库,所有读写都往里怼。等数据量起来、业务模块多了,痛点就一个个冒出来了:
- 报表统计要跑大查询,直接把生产库拖垮,前端核心交易跟着超时;
- 多个分支或系统都需要某几张基础表的数据,但又不希望他们直连生产库;
- 需要保留一份独立数据副本用于审计、数据分析,或者给测试环境提供接近真实的数据;
- 某些业务场景下,希望一个事务里的数据变更完整落到另一个库,不丢一半、不错一半。
这些诉求的共同点其实就一条:在不影响源库正常工作的前提下,把一份数据按需分发到另一个地方。你可以用 ETL 定时同步,也可以用备份恢复,还可以上 AlwaysOn 可用性组,但它们在延迟、表级粒度、对源库的影响上各有取舍。事务发布能在这几个维度里找到一个很实用的平衡点。
1.2 判断一个同步方案好不好,我只看这四点
我在选同步方案的时候,基本就看四个指标,其他都是次要的:
第一,对源库的影响。同步方案如果要在业务表上反复扫描、加锁、查询,那生产事故基本跑不掉。事务发布读取的是数据库事务日志,而不是直接去查业务表,这是它最大的优势。
第二,延迟窗口。核心业务表需要分钟级甚至秒级同步。定时的批量任务很难做到实时,而事务发布基于日志轮询,默认情况下端到端延迟通常在几秒到十几秒,满足大多数报表和数据分发场景。
第三,粒度灵活度。有些同步方案只能整库搬,事务发布可以精确到表,甚至能对行做过滤(比如只同步状态为正常的订单)、对列做裁剪(不包含敏感字段),这个灵活性非常实用。
第四,可维护性。事务发布提供了图形化管理工具、复制监视器、系统存储过程、状态表,而且代理机制能自愈式持续运行。运维的时候想知道它卡没卡、卡在哪,都有据可查。
这四个指标过一遍,事务发布适合的场景就很清晰了。
2. 事务发布的工作原理:三台机器和三个代理
2.1 三个角色:发布、分发、订阅
事务复制的拓扑结构通常涉及三个逻辑角色:
- 发布服务器(Publisher):源头数据库所在的实例,业务事务在这里发生。
- 分发服务器(Distributor):中转站,负责存储从发布端捕获到的待同步事务,并负责调度代理任务。
- 订阅服务器(Subscriber):目标库所在的实例,数据最终落到这里。
面包店老板出面包,这是发布;面包统一送到中转仓,这里就是分发;再从仓库配送到各家超市,就是订阅。分发服务器可以和发布服务器放在同一台机器上,也可以独立部署。为了分担 IO 压力,生产环境我更倾向于独立分发服务器,但小规模项目合并部署也完全能跑。
2.2 三个代理:快照、日志读取、分发
事务发布靠三个代理协同干活:
快照代理(Snapshot Agent):主要负责初始化。它会把发布表的结构、约束、索引以及存量数据生成一份快照,放到指定的共享目录中。订阅端在启动时会消费这份快照,这样后续的增量变更才有基准点。需要注意的是,快照代理只在初始化阶段运行,平时不需要一直干活。
日志读取代理(Log Reader Agent):这是事务发布的心脏。它定时扫描发布数据库的事务日志,把与发布表相关的变更(插入、更新、删除)解析成命令,写入分发数据库。默认情况下,它每 5 秒扫描一轮,这也是延迟的起点。
分发代理(Distribution Agent):把分发数据库中的命令按顺序推送到订阅端并执行。推式订阅时它运行在分发服务器上,拉式订阅时运行在订阅服务器上。一个发布对应多个订阅时,可以为每个订阅分别调度分发代理,互不干扰。
2.3 一条数据从源库到目标库的完整旅程
整个数据流可以描述成这样:
事务在发布库提交 → 事务日志写入重做记录 → 日志读取代理捕获与发布相关的变更 → 变更以命令形式从发布库传到分发库 → 分发代理按事务边界读取命令 → 命令应用于订阅库并提交。
这里有个关键点:事务发布是事务级一致的。一个源事务里的多表多条记录变更,在订阅端也会以同一个事务提交。要么全部应用成功,要么全部不应用。这样就不会出现主表同步过去了、明细表还停在老版本的“半同步”状态。这一点在实际业务中价值非常大,尤其是你有多个关联表联合同步的时候。
延迟主要发生在两个环节:日志读取代理的轮询间隔,以及分发代理在分发数据库中读取命令并执行的耗时。前者默认是 5 秒,后者取决于网络和订阅端负载。理解了这条链路,后面排查高延迟问题时思路就清晰了。
3. 对比一圈,为什么我最终选了事务发布
3.1 主流同步方案参数对比
经常和我讨论复制选型的朋友都会问:为什么不用 AlwaysOn?为什么不用 CDC?为什么不用作业调 SSIS?我把几个主流方案放在一个表里对比,用起来更直观:
| 方案 | 同步粒度 | 典型延迟 | 对源库影响 | 适用场景 |
|---|---|---|---|---|
| AlwaysOn 可用性组 | 整个数据库 | 秒级 | 日志重放,影响小 | 容灾、只读副本、高可用 |
| 数据库镜像 | 整个数据库 | 秒级 | 日志传输 | 已过时,被 AlwaysOn 替代 |
| 日志传送 | 整个数据库 | 分钟级 | 日志备份恢复 | 容灾备用库 |
| 事务复制 | 表、行、列可筛选 | 秒级 | 日志读取,影响小 | 表级分发、报表同步 |
| CDC | 表级变更捕获 | 需要自行消费 | 日志读取,影响小 | 数据管道、增量数仓 |
| ETL/SSIS 定时同步 | 可自定义 | 分钟/小时级 | 查询压力大 | 离线批量同步 |
事务复制不是全能的,但它在“表级”“秒级”“对源库影响小”这三个条件同时成立时,几乎是 SQL Server 环境里最顺手的方案。
3.2 哪些场景适合用事务发布
结合我的实际经验,下面这些场景很适合用事务发布:
报表库隔离。核心交易库上跑复杂统计查询,最稳妥的方式就是把需要统计的业务表同步到一台独立报表服务器,查询随便写,完全不担心影响生产。
数据分发。总部维护基础数据,比如商品、客户、组织架构,需要下发给多个分支机构或业务系统。一份发布,多个订阅,大家拿到的数据保持同步。
读写分离。主库承担写操作,订阅端承担读操作。注意,这要求应用层面把读写路由分开,订阅端尽量保持只读。
数据归集。多个业务库的表需要集中到一个平台库做统一查询时,可以用多个发布指向同一个订阅库。只要表名不冲突,事务复制能帮你省掉大量重复开发。
3.3 哪些场景打死也不用事务发布
需要完整数据库级容灾的场合。事务复制只同步发布项(表、视图、存储过程等),不会帮你同步登录名、作业、权限、SQL Agent 配置。真要做整体容灾,AlwaysOn 或日志传送更合适。
结构频繁变更的系统。表结构变化尤其是加列、改类型,事务发布不是不能应对,但很多情况需要重新初始化或重新生成快照。上线勤快的项目用事务复制,运维会非常酸爽。
双向同步。事务发布有一个“更新订阅”的选项,允许订阅端回写发布端,但冲突解决机制很复杂,生产中我不太建议轻易开启双向更新。如果你确实需要多地写入并合并,更好的选择是业务层的冲突设计,或者使用专门的数据合并中间件。
3.4 关于“更新订阅”选项,我的真实看法
有些教程会推荐开启可更新订阅来实现双向写,但我的建议是:默认关掉,订阅端保持只读。原因很简单,事务复制的核心设计目标是“单向、有序、可靠的传播”,一旦允许订阅读者改数据,你就要面对主键冲突、更新丢失、死锁等问题,处理成本远高于收益。真有双向数据合并需求时,从业务层面解决分发优先级,比在一套复制机制里硬解冲突靠谱得多。
4. 实操:从零搭一套事务发布(SSMS 版 + 脚本版)
4.1 动手前的前置准备
搭建之前,我习惯先花十分钟把环境确认清楚,避免后面反复跳坑:
- 版本和实例。发布库、分发库、订阅库实例的网络名必须能相互解析。特别提示:SQL Server 复制对 IP 地址支持很差,最好全程使用计算机名或 DNS 别名。我见过太多人用 IP 配置发布服务器,最后初始化过程各种迷之失败。
- SQL Agent 服务。所有复制代理都运行在 SQL Server Agent 下,Agent 服务没启动就等于没有搬运工。建议把 Agent 服务设为自动启动,且账号要有足够权限。
- 共享目录。快照代理会把初始化文件写入一个网络共享目录,比如
\\DIST_SERVER\ReplData。这个共享必须存在,并且运行 SQL Agent 服务的 Windows 账号要有读写权限。端口方面,文件共享走 445,SQL Server 走 1433,防火墙都要放行。 - 账号权限。分发代理连接发布端和订阅端时,需要拥有 db_owner 以上权限。最简单的做法是用 Windows 身份验证,前提是代理作业账号在两端都有权限;如果不行,就用 SQL 账号并设置好密码。
4.2 配置分发服务器
SSMS 里的路径是:在分发服务器上右键“复制” → “配置分发”。但我这里更想给出脚本方式,方便你们后续做自动化或者批量部署。
在分发服务器上执行以下脚本:
-- 将当前实例标记为分发服务器 USE master; GO EXEC sp_adddistributor @distributor = N'DIST_SERVER'; GO -- 创建分发数据库 distribution EXEC sp_adddistributiondb @database = N'distribution', @data_folder = N'D:\ReplData', @data_file = N'distribution', @log_folder = N'D:\ReplLog', @log_file = N'distribution_log'; GO -- 注册允许使用该分发服务器的发布服务器 EXEC sp_adddistpublisher @publisher = N'PUB_SERVER', @distribution_db = N'distribution', @working_directory = N'\\DIST_SERVER\ReplData', @security_mode = 1;这里解释几个参数:
@data_folder和@log_folder是分发数据库的数据文件和日志文件路径,建议按 D 盘独立存放,避免和系统库抢空间。@working_directory就是快照文件的工作目录,必须在分发服务器上创建共享目录。@security_mode = 1表示用 Windows 身份验证连接发布服务器。如果你的 SQL Agent 账号是域账号,两端都有权限,这就是最省心的方式。
4.3 在发布服务器上创建发布与文章
分发服务器配置好后,就去发布服务器上启用发布数据库,然后创建发布和文章。文章(Article)就是你要同步的具体表或视图。
-- 启用发布数据库 USE master; GO EXEC sp_replicationdboption @dbname = N'PubDB', @optname = N'publish', @value = N'true'; GO -- 进入发布库,创建发布 USE PubDB; GO EXEC sp_addpublication @publication = N'PubSync', @repl_freq = N'continuous', @status = N'active', @allow_push = N'true', @allow_pull = N'true', @sync_method = N'native'; GO -- 添加需要同步的表 EXEC sp_addarticle @publication = N'PubSync', @article = N'Orders', @source_object = N'Orders', @type = N'logbased'; GO如果你只需要同步订单表中状态为“完成”的记录,可以在添加文章时加过滤条件:
EXEC sp_addarticle @publication = N'PubSync', @article = N'Orders', @source_object = N'Orders', @type = N'logbased', @filter_clause = N'Status = 2';这样发布库里的数据变更,只有满足条件的那部分才会被复制。注意,过滤列的值一旦从 2 变成其他状态,订阅端也不会删除这条已经同步过的数据。这个细节容易被忽略,设计业务时要提前评估好。
用 SSMS 操作时,右键“本地发布” → “新建发布”,跟着向导选择表、勾选事务发布、设置快照代理和安全选项,流程等价。生产环境中多发布、多订阅的批量部署,脚本方式的可控性明显更好。
4.4 创建订阅并完成初始化
在订阅服务器上创建订阅。以推式订阅为例:
USE SubDB; GO EXEC sp_addsubscription @publication = N'PubSync', @subscriber = N'SUB_SERVER', @destination_db = N'SubDB', @subscription_type = N'push', @sync_type = N'automatic'; GO EXEC sp_addpushsubscription_agent @publication = N'PubSync', @subscriber = N'SUB_SERVER', @subscriber_db = N'SubDB', @job_login = N'sa', @job_password = N'your_password', @subscriber_security_mode = 1;@sync_type = N'automatic'表示订阅创建后会自动去应用快照,完成初始数据同步。这个过程会先复制表结构、索引,再把存量数据批量导入订阅端。
如果你希望订阅端自己保留一部分数据、只从某个时间点开始接收新变更,那可以考虑@sync_type = N'none',但前提是你必须保证两端数据基准完全一致,否则后续一致性会非常难查。我的建议是,除了通过备份初始化的大库场景,其他情况一律用快照初始化,简单且准确。
等订阅作业跑起来,可以在订阅端执行一个查询验证:
SELECT COUNT(*) FROM SubDB.dbo.Orders;如果行数和发布端一致,说明初始化成功,后续的增量数据会自动流过来。
4.5 初始化大库时的备份初始化思路
如果你的发布表有上亿行,快照初始化会非常慢,不仅占用大量磁盘空间,还可能把分发服务器的带宽打满。这种情况下更推荐备份初始化。
流程大概是:先在发布库做一次完整备份,把备份文件还原到订阅库。然后创建发布和订阅时,指定初始化方式为“从备份初始化”,而不是快照初始化。这样订阅库先有了和发布库一致的数据快照,日志读取代理随后从备份点开始捕获增量事务。
备份初始化解决的是“存量数据太大”的痛点,适合几十 GB 上百 GB 的大表。它需要对备份时间和备份点事务日志位置有准确掌握,操作复杂度比快照初始化高一些。我这边强烈建议先在一台测试服务器上完整演练一遍,确认订阅追平的时间窗口可接受,再上生产。
4.6 五分钟自检:同步是否真的在跑
配置完复制,我一般不会直接干等。我会做几个快速自检:
- 在发布端插入一条测试数据;
- 等十几秒,查询订阅端是否出现这条数据;
- 打开 SSMS 里的“复制监视器”,查看日志读取代理和分发代理的状态;
- 查看分发数据库中待分发的命令数量。
查询待分发命令数量的脚本可以这样写:
USE distribution; GO SELECT COUNT(*) AS PendingCommands FROM dbo.MSrepl_commands WITH (NOLOCK);只要这个数字持续增长但订阅端没有消费,那多半是分发代理出问题了;如果这个数字一直很小,说明链路是健康的,数据正在流动。
5. 生产环境里常踩的坑和排查思路
5.1 快照初始化失败的三个高发原因
快照代理在初始化阶段把文件写好、推送到订阅端,中间任何一个环节断了都可能导致失败。我遇到最多的三类原因:
第一,快照共享目录权限不够。SQL Agent 服务账号对\\DIST_SERVER\ReplData没有写入权限,日志里最常见的报错是“无法创建目录”或者“拒绝访问”。解决办法很简单,给账号 grant 共享目录的完全控制权,注意不光是 NTFS 权限,共享权限也要对。
第二,网络名解析问题。用 IP 地址配置发布服务器,或者计算机名解析异常时,快照代理去连接发布端就会失败。我处理过好几次这种情况:拔掉网线重插后机器名解析变了,结果复制马上报错。针对这种问题,我建议在 SQL Server 配置管理器里设置固定的网络别名(Alias),直接用别名指向发布实例。
第三,初始化快照中的表结构冲突。如果订阅端同名表已经存在,且表结构差异较大,快照应用时就会报错。最稳妥的方式是初始化前清空订阅端对应的表,或者新建一个空库交给复制管。
打开复制监视器,基本能定位到具体卡在哪个代理、哪个环节,这个操作比到处翻 SQL Server 错误日志要高效得多。
5.2 日志读取代理一直 Pending,或者数据不更新了
日志读取代理是事务发布的“汽油泵”,它一罢工,整个链路就停。典型特征:发布端仍然能写入,订阅端数据再也不更新,分发数据库里的MSrepl_commands表持续增长。
排查思路按顺序来:
- 在复制监视器里确认日志读取代理处于“正在运行”还是“失败”状态。
- 查看代理历史记录,看最后一条错误信息。
- 如果代理一直“正在运行”但没动作,可以手动停止后重新启动该代理作业,很多临时卡死能通过重启解决。
- 检查发布库的日志活跃情况,大事务或长时间未提交事务会导致日志读取代理等待。
- 如果
MSrepl_commands一直在增长,而分发代理没消费,问题更可能是出在分发代理这边,而不是日志读取代理。
有一个重要提醒:日志读取代理依赖“尚未分发的事务最早日志序列号”来定位读取起点,如果发布数据库的事务日志被极端情况截断,复制就会彻底失效,这种情况下只能重新初始化部分订阅。
5.3 机器名换了、IP 配了,复制全废
SQL Server 复制对服务器名的敏感程度超出很多人的预期。复制相关元数据里存量了许多端到端标识,一旦实例的机器名变了,发布、分发、订阅之间的信任关系就会断掉。
我有一个惨痛经验:某项目因为机房迁移,服务器改名,结果所有发布和订阅全部进入不可用状态,最后花了整整一个下午重新搭建和初始化。后来我养成了一个习惯,在迁移或调整机器名前,提前把所有复制配置导出、把所有订阅类型记录清楚,改完名后要么用注册别名的方式恢复,要么干脆重建复制链路。
另外还要留个心眼:SQL Server 实例内部记录的服务器名不一定和操作系统机器名一致。用下面这个语句可以确认实际标识:
SELECT @@SERVERNAME;如果这个值和你的机器名对不上,需要先用sp_dropserver配合sp_addserver修正,再操作复制。
5.4 遇到错误就跳过?先把一致性账算清
分发代理在应用命令时如果碰到错误,默认会停止。最常见的错误类型是主键冲突(错误号 2627)或唯一约束冲突(错误号 2601)。有些人会在代理启动参数里加-SkipErrors 2627,把这类错误跳过去,让复制继续跑。
我不是说这个参数不能用,但你要清楚代价:跳过的命令,在订阅端就没有被应用,数据会慢慢产生差异。如果你把 2627 直接跳过,那意味着订阅端缺少了这些更新过的记录,后续业务读出来的数据可能和源库不一致。
我的建议是:先用tablediff工具或自己写比对脚本来搞清楚数据差异有多大,再决定是手工补数据,还是直接重新初始化这一个订阅。跳过错误只能是“救火”手段,不能当日常处理方式。
tablediff在 SQL Server 安装目录的 COM 文件夹下,示例调用:
tablediff -sourceserver "PUB_SERVER" -sourcepubdb "PubDB" -sourcearticle "Orders" -destinationserver "SUB_SERVER" -destinationdb "SubDB"这个工具能按行比对两张表的数据差异,并生成修复 SQL,非常实用。
5.5 巡检脚本:每天花 30 秒看复制状态
生产环境里复制链路不能靠感觉,要有量化数据。我从实际维护中拆出了几个核心指标,做成一条巡检脚本,每天定时跑一遍,输出异常就告警。
需要关注的关键点有三类:
- 复制代理是否都在正常运行;
- 待分发命令数是否居高不下;
- 订阅端是否持续了一段时间没有再同步。
在分发数据库里查询分发代理的状态时,可以参考这段脚本:
USE distribution; GO SELECT status, start_time, duration, comments FROM dbo.MSdistribution_history ORDER BY time_stamp DESC;查询订阅端的同步延迟,我习惯对比两端表的更新时间戳。如果业务表有ModifyTime这类字段,直接取两端最新一条记录的时间差就行,比看系统内部计数器更直观。
6. 性能与长期维护心得
6.1 用参数调低延迟
如果你觉得默认的秒级延迟还不够,可以调整日志读取代理的参数。最常见的两个参数:
PollingInterval:日志读取代理轮询间隔,默认 5 秒。调到 1 秒会更快发现新事务,但也会增加代理的 CPU 开销。ReadBatchSize:每次从日志中读取的最大事务数量,默认 500。调高能提升批量处理能力,适合日志量大、事务频繁的库。
修改分发代理参数时,可以用sp_agentparameter操作。例如:
EXEC sp_agentparameter @profile_name = N'default', @parameter_name = N'PollingInterval', @parameter_value = 1;有一点要说清楚:低延迟和资源开销是对立的。不要为了追求 1 秒同步把 CPU 和 IO 都拉满。我的经验是,5 秒轮询对绝大多数报表同步业务已经完全够用,要追求亚秒级同步,那就不是事务复制擅长的领域了。
6.2 表设计和索引对复制的影响
复制表最好有明确的主键。日志读取代理在执行 UPDATE 和 DELETE 的复制时,需要通过主键或唯一索引在订阅端定位目标行。没有主键的表,虽然也能配置事务发布,但更新和删除操作的复制效率会非常差,极端情况下甚至无法复制。
发布端如果频繁更新大字段(例如text、ntext、image或大varchar),复制会将整条命令传输和重放,增加分发库体积和网络负担。对于这类表,更合理的方式是拆分字段,只复制业务需要的核心列。
订阅端的索引结构也要留意。默认情况下,事务发布会同步主键和部分约束,但你订阅端的查询需求可能不同。你可以对订阅端表额外建立索引来加速报表查询。注意:后续发布端结构变化重新初始化时,订阅端可能会被快照覆盖,届时索引需要重新补齐。
6.3 我的日常维护清单
复制跑起来只是开始,长期稳定才是目的。我自己的维护清单大致是这样:
- 每天检查复制代理状态和待分发命令数,异常及时处理;
- 每周检查分发数据库的大小,必要时清理历史数据;
- 每月做一次两端抽样数据比对,用
tablediff或者自写脚本; - 大版本升级、结构调整前,提前做复制配置备份和订阅端快照;
- 订阅端如果长期不消费,先把订阅停掉,不要任其拖着无界增长的事务队列。
这套维护习惯在多个项目里跑下来,最大的收益不是少出故障,而是每次故障都能在早期被肉眼察觉,处理成本非常低。
最后再分享一个我自己的习惯:每逢大版本变更或索引调整后,我都会重建快照并重新初始化订阅。道理很简单,复制链路是一个长跑项目,关注的不只是搭建那一刻的顺利,而是长时间运行中的数据质量。把日常巡检脚本放进作业里,每周看一眼延迟和错误,剩下的就交给时间去验证。这套方案成熟归成熟,真正让它稳定发挥价值的,还是背后持续的维护功夫。